From 2f6157d8df85d0e525f1539fa484deb774c9be45 Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Thu, 24 Sep 2026 12:58:59 -0700 Subject: [PATCH 01/11] feat: formalize Squad charter profile Part of #2069 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> fix: complete charter profile conformance Part of #2069 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> docs: remove specification trailing whitespace Part of #2069 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Source-Commit: d21ab99fa90c9df315b2897123f74231d12c1656 --- .changeset/formalize-charter-profile.md | 5 + docs/specification/README.md | 38 ++ docs/specification/artifact-inventory.md | 54 ++ docs/specification/charter-v0.1.md | 242 +++++++++ docs/specification/core-v0.1.md | 145 ++++++ packages/squad-sdk/CHANGELOG.md | 11 + .../squad-sdk/src/agents/charter-compiler.ts | 276 +++++++---- .../squad-sdk/src/agents/charter-markdown.ts | 225 +++++++++ .../squad-sdk/src/agents/charter-validator.ts | 469 ++++++++++++++++++ packages/squad-sdk/src/agents/index.ts | 17 + packages/squad-sdk/src/parsers.ts | 29 ++ packages/squad-sdk/src/state/io/charter-io.ts | 89 +++- packages/squad-sdk/src/types.ts | 9 + .../charter-v0.1/legacy/template-style.md | 21 + test-fixtures/spec/charter-v0.1/manifest.json | 87 ++++ .../charter-v0.1/negative/invalid-behavior.md | 19 + .../spec/charter-v0.1/positive/canonical.md | 19 + .../round-trip/extensions.edited.md | 54 ++ .../charter-v0.1/round-trip/extensions.md | 50 ++ test/charter-spec.test.ts | 397 +++++++++++++++ 20 files changed, 2156 insertions(+), 100 deletions(-) create mode 100644 .changeset/formalize-charter-profile.md create mode 100644 docs/specification/README.md create mode 100644 docs/specification/artifact-inventory.md create mode 100644 docs/specification/charter-v0.1.md create mode 100644 docs/specification/core-v0.1.md create mode 100644 packages/squad-sdk/src/agents/charter-markdown.ts create mode 100644 packages/squad-sdk/src/agents/charter-validator.ts create mode 100644 test-fixtures/spec/charter-v0.1/legacy/template-style.md create mode 100644 test-fixtures/spec/charter-v0.1/manifest.json create mode 100644 test-fixtures/spec/charter-v0.1/negative/invalid-behavior.md create mode 100644 test-fixtures/spec/charter-v0.1/positive/canonical.md create mode 100644 test-fixtures/spec/charter-v0.1/round-trip/extensions.edited.md create mode 100644 test-fixtures/spec/charter-v0.1/round-trip/extensions.md create mode 100644 test/charter-spec.test.ts diff --git a/.changeset/formalize-charter-profile.md b/.changeset/formalize-charter-profile.md new file mode 100644 index 000000000..add45dada --- /dev/null +++ b/.changeset/formalize-charter-profile.md @@ -0,0 +1,5 @@ +--- +'@bradygaster/squad-sdk': minor +--- + +Add the draft `squad-charter/v0.1` artifact profile, a shared deterministic Markdown scanner, typed validator diagnostics and capability metadata, canonical and lossless extension-preserving serialization, mandatory conformance fixtures, and implementation-independent specification documents. diff --git a/docs/specification/README.md b/docs/specification/README.md new file mode 100644 index 000000000..aeeb93585 --- /dev/null +++ b/docs/specification/README.md @@ -0,0 +1,38 @@ +# Squad Specification + +**Core version:** 0.1 + +**Status:** Draft + +**Document class:** Informative index; linked core/profile documents identify +their normative sections +**First artifact profile:** [Squad Charter Profile v0.1](charter-v0.1.md) + +This directory defines portable contracts for the on-disk artifacts that make +up a Squad. The specification is independent of the TypeScript reference +implementation. A producer or consumer can implement one artifact profile +without claiming that an entire Squad is conformant. + +## Documents + +- [Core v0.1](core-v0.1.md) defines shared terminology, conformance classes, + versions, paths, extensions, and trust requirements. +- [Charter Profile v0.1](charter-v0.1.md) defines the first normative artifact + profile for `.squad/agents/{id}/charter.md`. +- [Artifact inventory](artifact-inventory.md) records the current on-disk + surface, parser locations, maturity, and proposed profile order. +- [`test-fixtures/spec/charter-v0.1/`](../../test-fixtures/spec/charter-v0.1/) + contains language-neutral positive, negative, legacy, and round-trip + fixtures. + +Normative references are maintained in +[Core v0.1 §2](core-v0.1.md#2-normative-references), including BCP 14, +RFC 3339, and CommonMark 0.31.2. + +## Versioning policy + +The core and each artifact profile are versioned independently. Draft `0.x` +documents may change before publication, and unsupported versions fail closed. +After publication, a revision is immutable: editorial corrections are recorded +as dated errata, while behavioral changes require a new profile version. See +[Core v0.1 §10](core-v0.1.md#10-publication-errata-and-versioning). diff --git a/docs/specification/artifact-inventory.md b/docs/specification/artifact-inventory.md new file mode 100644 index 000000000..8c4047d17 --- /dev/null +++ b/docs/specification/artifact-inventory.md @@ -0,0 +1,54 @@ +# Squad Artifact Inventory + +**Audit date:** 2026-09-24 + +**Purpose:** Profile planning and parser-drift evidence for issue #2069. + +Paths in this table are Squad-root-relative. Repository-local installations +render them beneath `.squad/`. + +| Artifact | Artifact class | Current authority/consumer | Maturity | Proposed profile | +|---|---|---|---|---| +| `agents/{id}/charter.md` | required | `packages/squad-sdk/src/agents/charter-compiler.ts` | Draft profile shipped here | Charter v0.1 | +| `team.md` | required | config Markdown migration, presets, coordinator | Multiple table shapes remain | Roster and routing | +| `routing.md` | required | `packages/squad-sdk/src/config/routing.ts` | Parsed but not formally versioned | Roster and routing | +| `config.json` | optional | config schema/loaders | Structured schema exists | Configuration and casting | +| `casting/registry.json` | runtime-state | casting provenance/state APIs | Versioned implementation schema | Configuration and casting | +| `casting/history.json` | runtime-state | casting atomic update path | Backend-sensitive | Configuration and casting | +| `skills/{name}/SKILL.md` | optional | skill loader and coordinator discovery | Frontmatter conventions exist | Skills | +| `decisions.md` | runtime-state | state bridge, coordinator, Scribe | Authority/backend semantics evolving | Decisions and memory | +| `decisions/inbox/**` | runtime-state | governed state bridge | Drop-box protocol, backend-sensitive | Decisions and memory | +| `agents/{id}/history.md` | runtime-state | history/state APIs | Backend-sensitive | Decisions and memory | +| `identity/**` | runtime-state | coordinator/state bridge | Backend-sensitive | Deferred state profile | +| generated capability blocks | generated | `team-capabilities` generator | Reproducible output | Implementation profile | +| `.github/agents/squad.agent.md` | generated/runtime prompt | Copilot surfaces | Product integration artifact | Implementation profile | + +## Charter conformance audit + +The active repository charters demonstrate three families: + +1. Specialist charters use `Identity ID`, `Identity Purpose`, and the canonical + responsibility headings selected by Charter Profile v0.1. +2. Built-in support charters use legacy `Name`/`Role` identity fields and + `What I Own`/`Boundaries` variants. +3. The distributed charter template uses legacy `Name`, `Role`, `Expertise`, + `Style`, `What I Own`, `Boundaries`, and `Collaboration`. + +Before this profile, the parser recognized only the third family for +responsibility sections, while the active specialists used the first. It also +parsed `Reasoning Effort` and `Context Tier`, but the charter serializer omitted +both fields. The reference implementation now recognizes canonical and legacy +aliases, preserves both behavioral fields in serialization, and exposes a +deterministic validator. + +## Profile sequencing + +1. Charter v0.1: identity, responsibility semantics, behavioral preferences, + diagnostics, legacy aliases, and fixtures. +2. Roster and routing: joins among agent ID, directory, roster, and ownership. +3. Configuration and casting: schemas, provenance, revisions, and precedence. +4. Skills: discovery, metadata, trust, and compatibility. +5. Decisions, memory, and mutable state after backend authority stabilizes. + +Runtime protocols, prompts, CLI UX, telemetry, and hosted publication remain +informative or implementation-specific until a profile explicitly adopts them. diff --git a/docs/specification/charter-v0.1.md b/docs/specification/charter-v0.1.md new file mode 100644 index 000000000..a2d8041c1 --- /dev/null +++ b/docs/specification/charter-v0.1.md @@ -0,0 +1,242 @@ +# Squad Charter Profile v0.1 + + + +**Status:** Draft + +**Document class:** Normative unless a section is marked informative + +**Profile identifier:** `squad-charter/v0.1` + +**Artifact class:** required + +**Squad-root-relative path:** `agents/{id}/charter.md` + +**Repository-relative example:** `.squad/agents/{id}/charter.md` + +## 1. Purpose and authority + +A charter identifies one agent and supplies human-readable operating guidance +plus optional runtime preferences. Team membership, routing ownership, +authorization, and configuration overrides are outside this profile. + +The path segment `{id}` is the reference identity. When profile path context is +available, `Identity ID` MUST equal `{id}` after converting Windows separators +to `/`. Paths that are neither exactly `agents/{id}/charter.md` nor terminated +by `.squad/agents/{id}/charter.md` provide no profile path context. + +## 2. Association and unsupported versions + +The artifact path associates a charter with the charter artifact family. The +profile version is selected explicitly by the calling API or surrounding +manifest; v0.1 has no in-document version marker. + +A consumer asked to process a profile other than one it supports MUST return an +unsupported-profile error and MUST NOT reinterpret the document as v0.1. + +## 3. Canonical document shape + +```markdown +# Display Name — Role + +## Identity +- **ID:** `agent-id` +- **Purpose:** Non-empty single-line purpose text. + +## Accountable Responsibilities +... + +## Non-Responsibilities +... + +## Collaboration and Review Authority +... + +## Model +- **Preferred:** auto +- **Reasoning Effort:** auto +- **Context Tier:** auto +``` + +The H1, `Identity`, and three canonical responsibility sections define the +canonical presentation. `Model` is optional. Producers MUST emit canonical +responsibility headings unless an API caller explicitly requests legacy +output. + +`Purpose` is one non-empty, single-line Markdown field value. The profile does +not attempt to determine whether prose is grammatically a sentence. + +## 4. Deterministic Markdown subset + +Structural recognition uses these rules, in order: + +1. Only ATX H1 and H2 headings beginning in column one are structural. Setext + headings and H3-H6 headings are prose. +2. A machine field is a single line with zero to three leading spaces, an + optional list marker, then `**Label:** value`. +3. Headings and fields inside fenced code are ignored. Fences use at least + three backticks or tildes, may have up to three leading spaces, and close + with the same character and at least the opening length. +4. Lines in indented code (four spaces or one tab) and block quotes are ignored. +5. A line whose first non-space characters are ``. +6. The first `X-` H2 begins the opaque extension tail. Within that tail, only + another `X-` H2 starts a new extension. Standard-looking headings and fields + in extension bodies are opaque text. +7. Canonical producers MUST place all standard sections before the first + extension section. + +These rules are the required observable behavior. Implementations need not use +the same scanner internally. + +## 5. Machine-readable fields + +Labels and standard section names are ASCII case-insensitive. Every field in +this table has cardinality zero-or-one within its owning section. + +| Section | Field | Canonical requirement | Semantics | +|---|---|---:|---| +| Identity | `ID` | required | Lowercase kebab-case: `[a-z0-9]+(?:-[a-z0-9]+)*` | +| Identity | `Purpose` | required | Non-empty single-line purpose | +| Identity | `Name` | legacy optional | Display name | +| Identity | `Role` | legacy optional | Role | +| Identity | `Expertise` | legacy optional | Comma-separated informative list | +| Identity | `Style` | legacy optional | Informative style text | +| Model | `Preferred` | optional | Model identifier or `auto` | +| Model | `Rationale` | optional | Informative prose | +| Model | `Fallback` | legacy optional | Informative hint, not a portable fallback chain | +| Model | `Reasoning Effort` | optional | `auto`, `low`, `medium`, `high`, `xhigh`, or `max` | +| Model | `Context Tier` | optional | `auto`, `default`, or `long_context` | + +Every standard semantic section also has cardinality zero-or-one. A canonical +heading and its legacy alias count as the same semantic section. + +Missing behavioral fields mean no charter preference. `auto` preserves the +author's request for runtime selection and MUST NOT become a literal runtime +override. Unsupported model identifiers MAY be preserved as authored text but +MUST NOT be claimed as executable without runtime support. Invalid reasoning +or context values are validation errors and MUST NOT become runtime overrides. + +## 6. Canonical and legacy handling + +Consumers MAY accept these legacy aliases: + +| Legacy form | Canonical semantic | +|---|---| +| H1 identity without `Identity ID` | Display name and role only | +| `Identity Name` | Display name | +| `Identity Role` | Role | +| `Identity Expertise` | Informative expertise list | +| `Identity Style` | Informative style | +| `What I Own` | `Accountable Responsibilities` | +| `Boundaries` | `Non-Responsibilities` | +| `Collaboration` | `Collaboration and Review Authority` | + +A document with only warning diagnostics is **noncanonical-compatible**, not +canonical. Validators expose both canonical `conforms` and broader `accepted` +status. Legacy output requires an explicit editor mode. + +## 7. Extensions + +Extension headings use `## X--`. Canonical names use lowercase +kebab-case namespace and name segments. Names compare case-insensitively; two +headings that differ only by case collide and are invalid. + +The namespace owner defines extension semantics. `X-squad-*` is reserved for +the Squad specification project; third parties MUST use a namespace they +control. Unknown consumers MUST treat extension bodies as opaque, untrusted +bytes. + +Editors: + +- MUST return the original bytes for a canonical no-op round trip; +- MUST preserve every unknown extension section byte-for-byte and in relative + extension order during known-field edits; +- MAY rewrite known standard sections into canonical form; +- MUST NOT interpret extension body content as standard charter structure. + +## 8. Diagnostics + +Missing constructs are anchored at line 1 or, when present, the nearest owning +H1/H2 heading. Duplicate diagnostics point to the second and later occurrence. +Diagnostics sort by line, column, then the code order below. + +| Code | Severity | Condition | +|---|---|---| +| `SQC001` | error | Empty charter | +| `SQC002` | error | Missing H1 identity | +| `SQC003` | warning | Noncanonical H1 | +| `SQC004` | error | Duplicate machine-readable field | +| `SQC005` | error | Invalid Identity ID | +| `SQC006` | error | Empty Preferred model | +| `SQC007` | error | Invalid Reasoning Effort | +| `SQC008` | error | Invalid Context Tier | +| `SQC009` | error | Empty Identity Purpose | +| `SQC010` | error | Identity ID/path mismatch | +| `SQC011` | error | Duplicate standard semantic section | +| `SQC012` | error | Case-insensitive extension collision | +| `SQC013` | error | Unsupported profile | +| `SQC101` | warning | Missing canonical Identity section | +| `SQC102` | warning | Missing canonical Identity ID | +| `SQC103` | warning | Missing canonical Identity Purpose | +| `SQC104` | warning | Missing canonical responsibility section | +| `SQC105` | warning | Legacy responsibility headings | +| `SQC106` | warning | Noncanonical extension heading | + +## 9. Operational capabilities + +Implementations claim operations individually: + +| Capability | Required behavior | +|---|---| +| `squad-charter/v0.1/parse` | Parse standard fields with the deterministic subset | +| `squad-charter/v0.1/validate` | Emit the diagnostics and classification above | +| `squad-charter/v0.1/edit` | Canonical serialization plus lossless extension preservation | +| `squad-charter/v0.1/legacy-consume` | Accept documented legacy aliases without calling them canonical | + +The TypeScript reference implementation exports all four identifiers. A +validation result reports only the validator capability because running +validation does not prove that parsing or editing was exercised. + +## 10. Security + +Charters are instructions, not authorization. A runtime MUST independently +enforce tool, filesystem, network, model, and review policy. It MUST NOT grant +capabilities because a charter requests them. Links, commands, encoded text, +HTML comments, code blocks, and extension content are untrusted data. + +## 11. Mandatory conformance suite + +Implementations claiming a capability MUST run the applicable cases in +`test-fixtures/spec/charter-v0.1/manifest.json`. The suite defines exact +diagnostic code, severity, location, ordering, path behavior, parsed semantics, +capability reporting, profile handling, and round-trip output. It includes +every standard field and section duplicate, fenced and opaque fake structure, +CRLF behavior, path mismatch, extension ordering, canonical output, legacy +ingestion, and `auto` runtime semantics. + +## 12. Informative TypeScript API example + +```ts +import { + CHARTER_CAPABILITIES, + parseCharterMarkdown, + serializeCharter, + validateCharterMarkdown, +} from '@bradygaster/squad-sdk/parsers'; + +const validation = validateCharterMarkdown(markdown, { + profile: 'squad-charter/v0.1', + path: '.squad/agents/reviewer/charter.md', +}); + +if (!validation.accepted) { + throw new Error(validation.diagnostics[0]?.message); +} + +const charter = parseCharterMarkdown(markdown); +charter.identity.purpose = 'Review public API compatibility.'; +const updated = serializeCharter(charter); + +console.log(CHARTER_CAPABILITIES.edit, updated); +``` diff --git a/docs/specification/core-v0.1.md b/docs/specification/core-v0.1.md new file mode 100644 index 000000000..52c0e6038 --- /dev/null +++ b/docs/specification/core-v0.1.md @@ -0,0 +1,145 @@ +# Squad Specification Core v0.1 + +**Status:** Draft + +**Document class:** Normative unless a section is marked informative + +**Capability identifier:** `squad-core/v0.1` + +## 1. Scope + +The core defines rules shared by separately versioned Squad artifact profiles. +It does not define runtime prompts, CLI commands, storage algorithms, +telemetry, GitHub workflows, or a whole-Squad conformance claim. + +## 2. Normative references + +- [BCP 14](https://www.rfc-editor.org/info/bcp14), comprising RFC 2119 and + RFC 8174, defines the requirement keywords used by this specification. +- [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) defines timestamp syntax. +- [CommonMark 0.31.2](https://spec.commonmark.org/0.31.2/) defines the Markdown + terminology used by profiles. A profile may select a deterministic subset + rather than require a complete CommonMark implementation. + +The keywords **MUST**, **MUST NOT**, **REQUIRED**, **SHOULD**, **SHOULD NOT**, +and **MAY** are normative only when shown in uppercase. + +## 3. Terms and conformance classes + +- **Squad root:** the `.squad/` directory selected by an implementation. +- **Artifact:** a file or directory beneath the Squad root described by a + profile. +- **Producer:** creates or updates an artifact. +- **Consumer:** interprets an artifact. +- **Validator:** returns deterministic diagnostics for a profile. +- **Editor:** reads and writes an artifact. +- **Runtime:** uses one or more artifacts to execute behavior. + +An implementation MUST declare exact operational capability identifiers. +Supporting one profile operation MUST NOT be represented as support for every +operation in that profile or for every Squad artifact. + +## 4. Root discovery, paths, and containment + +An implementation MUST resolve one Squad root before resolving artifact paths. +Repository-local state uses `/.squad/`. Configuration MAY select an +external or remote root. + +Every profile path is **Squad-root-relative** and never includes the `.squad/` +directory itself. For example, `agents/{id}/charter.md` is the profile path; +`.squad/agents/{id}/charter.md` is its repository-relative rendering. + +Consumers MUST reject references that escape the selected root after +normalization. Symlinks, junctions, mount points, and other reparse points MUST +NOT be followed when a profile prohibits traversal. + +## 5. Artifact classes + +Every profile MUST assign one of these classes: + +- **required:** required for the declared profile capability; +- **optional:** portable when present; +- **generated:** reproducible output, not an authority source; +- **runtime-state:** mutable state whose persistence may be backend-owned; +- **implementation-private:** outside portable conformance. + +Classification is profile-specific and MUST NOT be interpreted as a +requirement that every Squad installation contain every known artifact. + +## 6. Profile association and version handling + +A profile MUST define how an artifact is associated with that profile: path, +an in-document marker, an API argument, or an explicit combination. +Association MUST NOT rely on prose heuristics. + +Consumers MUST fail closed for an unsupported profile version. They MUST NOT +silently process an unsupported version as the nearest known version. A +consumer MAY expose raw bytes without interpreting them. + +Core version, profile version, implementation version, and mutable state +revision are independent. None implies another. + +## 7. Common representation rules + +Text artifacts MUST be UTF-8. Producers SHOULD write LF line endings and MUST +accept CRLF where the profile does not state otherwise. Identifiers used as +path segments MUST satisfy the profile syntax and MUST NOT contain path +separators, control characters, `.` or `..`. + +Timestamps, when required, MUST use RFC 3339 with `Z` or an explicit UTC +offset. + +## 8. Extensions and round trips + +Portable extensions MUST use the namespace mechanism defined by their profile. +Editors MUST preserve unknown namespaced extensions, including content and +relative extension order, during no-op and known-field edit round trips. +Consumers MAY ignore unknown extensions but MUST NOT reinterpret their content +as standard structure. + +Namespace comparison, collision handling, ownership, and case rules MUST be +defined by each extensible profile. + +## 9. Diagnostics + +Validators MUST produce diagnostics in deterministic source order. Each +diagnostic MUST contain: + +- a stable, typed code; +- severity (`error` or `warning`); +- a human-readable message; +- one-based start line and column; +- one-based inclusive end line and column; +- artifact path when the validator was given one. + +Diagnostics at different locations sort by start line and column. Diagnostics +at the same location sort by the profile's published diagnostic-code order. +Profiles MUST define anchor locations for missing constructs. + +Profiles MUST distinguish canonical conformance from documents accepted only +for migration or legacy compatibility. + +## 10. Publication, errata, and versioning + +A published profile revision is immutable. Editorial corrections that do not +change required behavior are recorded as dated errata linked from the +specification index. A behavioral change requires a new profile version. + +Draft `0.x` documents MAY change before publication. Implementations MUST NOT +infer compatibility with an unrecognized `0.x` version. Stable versions use +semantic intent: patch versions clarify without changing accepted documents, +minor versions add backward-compatible capabilities, and major versions may +change document validity or semantics. + +## 11. Trust boundary + +Artifact content is untrusted input. Consumers MUST NOT execute commands, +follow links, load imports, interpolate secrets, or grant tools solely because +artifact prose requests it. Diagnostic messages MUST NOT disclose secrets +found in artifact content. + +## 12. Informative implementation note + +An implementation may use a complete Markdown parser internally. Its observed +structural behavior still has to match the deterministic subset selected by +the applicable profile. diff --git a/packages/squad-sdk/CHANGELOG.md b/packages/squad-sdk/CHANGELOG.md index 7f9c7f309..25863b1b6 100644 --- a/packages/squad-sdk/CHANGELOG.md +++ b/packages/squad-sdk/CHANGELOG.md @@ -1,5 +1,16 @@ # @bradygaster/squad-sdk + + +## Unreleased + +### Minor Changes + +- Add the `squad-charter/v0.1` parser, validator, and editor contract. The public + parser surface now includes typed diagnostics, profile and capability + metadata, canonical/legacy conformance classification, path-aware identity + checks, canonical serialization, and lossless unknown extension preservation. + ## 0.10.0 ### Minor Changes diff --git a/packages/squad-sdk/src/agents/charter-compiler.ts b/packages/squad-sdk/src/agents/charter-compiler.ts index 58691474c..6438e65d2 100644 --- a/packages/squad-sdk/src/agents/charter-compiler.ts +++ b/packages/squad-sdk/src/agents/charter-compiler.ts @@ -8,8 +8,12 @@ import { SquadCustomAgentConfig } from '../adapter/types.js'; import { ConfigurationError } from '../adapter/errors.js'; -import { normalizeEol } from '../utils/normalize-eol.js'; import { VALID_REASONING_EFFORTS, VALID_CONTEXT_TIERS } from '../config/models.js'; +import { + scanCharterMarkdown, + type CharterMarkdownField, + type CharterMarkdownScan, +} from './charter-markdown.js'; /** Set form for fast lookup. */ const VALID_EFFORTS = new Set(VALID_REASONING_EFFORTS); @@ -66,16 +70,18 @@ export interface CharterConfigOverrides { * Parsed charter structure. */ export interface ParsedCharter { - /** Identity section: name, role, expertise, style */ + /** Identity section: canonical ID/purpose plus legacy descriptive fields */ identity: { + id?: string; + purpose?: string; name?: string; role?: string; expertise?: string[]; style?: string; }; - /** What I Own section content */ + /** Accountable Responsibilities section content, or legacy What I Own */ ownership?: string; - /** Boundaries section content */ + /** Non-Responsibilities section content, or legacy Boundaries */ boundaries?: string; /** Model preference from ## Model section */ modelPreference?: string; @@ -85,12 +91,31 @@ export interface ParsedCharter { modelFallback?: string; /** Reasoning effort preference from ## Model section */ reasoningEffort?: string; + /** Authored reasoning effort, including auto or an unsupported value */ + authoredReasoningEffort?: string; /** Context tier preference from ## Model section */ contextTier?: string; - /** Collaboration section content */ + /** Authored context tier, including auto or an unsupported value */ + authoredContextTier?: string; + /** Collaboration and Review Authority content, or legacy Collaboration */ collaboration?: string; /** Full charter content */ fullContent: string; + /** Source metadata used by lossless charter editors */ + source?: CharterSourceMetadata; +} + +/** + * Source representation retained for lossless no-op and extension-preserving + * charter edits. + */ +export interface CharterSourceMetadata { + /** Semantic snapshot captured when the source was parsed */ + semanticKey: string; + /** Whether the source already uses the canonical responsibility headings */ + canonicalShape: boolean; + /** Unknown namespaced extension sections in source order, byte-for-byte */ + extensions: readonly string[]; } /** @@ -176,22 +201,31 @@ export function compileCharterFull(options: CharterCompileOptions): CompiledChar ? `Expertise: ${parsed.identity.expertise.join(', ')}` : `${role || 'Agent'}`; - // Resolve model: config override > charter preference - const resolvedModel = configOverrides?.model || parsed.modelPreference; + // Resolve model: config override > charter preference. "auto" is authored + // intent for runtime selection, never a literal model override. + const configuredModel = configOverrides?.model?.trim(); + const charterModel = parsed.modelPreference?.trim(); + const resolvedModel = configuredModel?.toLowerCase() === 'auto' + ? undefined + : configuredModel || (charterModel?.toLowerCase() === 'auto' ? undefined : charterModel); // Resolve reasoning effort: config override > charter preference // Normalize: "auto" and invalid values resolve to undefined const configEffort = configOverrides?.reasoningEffort?.toLowerCase(); const charterEffort = parsed.reasoningEffort; // already validated during parsing const validConfigEffort = configEffort && configEffort !== 'auto' && VALID_EFFORTS.has(configEffort) ? configEffort : undefined; - const resolvedReasoningEffort = validConfigEffort || charterEffort; + const resolvedReasoningEffort = configEffort === 'auto' + ? undefined + : validConfigEffort || charterEffort; // Resolve context tier: config override > charter preference // Normalize: "auto" and invalid values resolve to undefined const configTier = configOverrides?.contextTier?.toLowerCase(); const charterTier = parsed.contextTier; // already validated during parsing const validConfigTier = configTier && configTier !== 'auto' && VALID_TIERS.has(configTier) ? configTier : undefined; - const resolvedContextTier = validConfigTier || charterTier; + const resolvedContextTier = configTier === 'auto' + ? undefined + : validConfigTier || charterTier; // Resolve tools: config override > charter-extracted tools const resolvedTools = configOverrides?.tools; @@ -231,7 +265,7 @@ export function compileCharterFull(options: CharterCompileOptions): CompiledChar * @returns Parsed charter structure */ export function parseCharterMarkdown(content: string): ParsedCharter { - content = normalizeEol(content); + const scan = scanCharterMarkdown(content); const result: ParsedCharter = { identity: {}, fullContent: content, @@ -241,108 +275,172 @@ export function parseCharterMarkdown(content: string): ParsedCharter { return result; } - // Extract ## Identity section - const identityMatch = content.match(/##\s+Identity\s*\n([\s\S]*?)(?=\n##|\n---|$)/i); - if (identityMatch) { - const identityContent = identityMatch[1]!; - - // Parse identity fields - const nameMatch = identityContent.match(/\*\*Name:\*\*\s*(.+)/i); - if (nameMatch) result.identity.name = nameMatch[1]!.trim(); - - const roleMatch = identityContent.match(/\*\*Role:\*\*\s*(.+)/i); - if (roleMatch) result.identity.role = roleMatch[1]!.trim(); - - const expertiseMatch = identityContent.match(/\*\*Expertise:\*\*\s*(.+)/i); - if (expertiseMatch) { - result.identity.expertise = expertiseMatch[1]! + const identityFields = fieldsInSection(scan, 'Identity'); + const id = findField(identityFields, 'ID'); + if (id) result.identity.id = stripCode(id.value); + + const purpose = findField(identityFields, 'Purpose'); + if (purpose) result.identity.purpose = purpose.value.trim(); + + const name = findField(identityFields, 'Name'); + if (name) result.identity.name = name.value.trim(); + + const role = findField(identityFields, 'Role'); + if (role) result.identity.role = role.value.trim(); + + const expertise = findField(identityFields, 'Expertise'); + if (expertise) { + result.identity.expertise = expertise.value .split(',') .map(e => e.trim()) .filter(e => e.length > 0); - } - - const styleMatch = identityContent.match(/\*\*Style:\*\*\s*(.+)/i); - if (styleMatch) result.identity.style = styleMatch[1]!.trim(); } + const style = findField(identityFields, 'Style'); + if (style) result.identity.style = style.value.trim(); + // Legacy charters identify the agent in the H1 instead of an Identity section. if (!result.identity.name || !result.identity.role) { - const titleMatch = content.match( - /^#\s+(.+?)\s+(?:—|–|-)\s+(.+?)\s*$/m, - ); + const titleMatch = scan.h1?.name.match(/^(.+?)\s+(?:—|–|-)\s+(.+?)$/); if (titleMatch) { result.identity.name ??= titleMatch[1]!.trim(); result.identity.role ??= titleMatch[2]!.trim(); } } - // Extract ## What I Own section - const ownershipMatch = content.match(/##\s+What I Own\s*\n([\s\S]*?)(?=\n##|\n---|$)/i); - if (ownershipMatch) { - result.ownership = ownershipMatch[1]!.trim(); - } + // Extract canonical ownership section or its legacy alias. + result.ownership = findSectionBody(scan, 'Accountable Responsibilities', 'What I Own'); - // Extract ## Boundaries section - const boundariesMatch = content.match(/##\s+Boundaries\s*\n([\s\S]*?)(?=\n##|\n---|$)/i); - if (boundariesMatch) { - result.boundaries = boundariesMatch[1]!.trim(); - } + // Extract canonical non-responsibilities section or its legacy alias. + result.boundaries = findSectionBody(scan, 'Non-Responsibilities', 'Boundaries'); // Extract ## Model section - const modelMatch = content.match(/##\s+Model\s*\n([\s\S]*?)(?=\n##|\n---|$)/i); - if (modelMatch) { - const modelContent = modelMatch[1]!; - const preferredMatch = modelContent.match(/\*\*Preferred:\*\*\s*(.+)/i); - if (preferredMatch) { - result.modelPreference = preferredMatch[1]!.trim(); - } - const rationaleMatch = modelContent.match(/\*\*Rationale:\*\*\s*(.+)/i); - if (rationaleMatch) { - result.modelRationale = rationaleMatch[1]!.trim(); - } - const fallbackMatch = modelContent.match(/\*\*Fallback:\*\*\s*(.+)/i); - if (fallbackMatch) { - result.modelFallback = fallbackMatch[1]!.trim(); - } - const effortMatch = modelContent.match(/\*\*Reasoning Effort:\*\*\s*(.+)/i); - if (effortMatch) { - const raw = effortMatch[1]!.trim().toLowerCase(); - // Normalize: "auto" → undefined, invalid values → undefined - if (raw !== 'auto' && VALID_EFFORTS.has(raw)) { - result.reasoningEffort = raw; - } else if (raw !== 'auto') { - // Surface invalid charter input to the author instead of dropping it silently. - console.warn( - `[squad] charter parse: ignoring invalid reasoning effort "${raw}" ` - + `(expected ${VALID_REASONING_EFFORTS.join(', ')}, or auto)`, - ); - } - } - const tierMatch = modelContent.match(/\*\*Context Tier:\*\*\s*(.+)/i); - if (tierMatch) { - const raw = tierMatch[1]!.trim().toLowerCase(); - // Normalize: "auto" → undefined, invalid values → undefined - if (raw !== 'auto' && VALID_TIERS.has(raw)) { - result.contextTier = raw; - } else if (raw !== 'auto') { - // Surface invalid charter input to the author instead of dropping it silently. - console.warn( - `[squad] charter parse: ignoring invalid context tier "${raw}" ` - + `(expected ${VALID_CONTEXT_TIERS.join(', ')}, or auto)`, - ); - } + const modelFields = fieldsInSection(scan, 'Model'); + const preferred = findField(modelFields, 'Preferred'); + if (preferred) result.modelPreference = preferred.value.trim(); + + const rationale = findField(modelFields, 'Rationale'); + if (rationale) result.modelRationale = rationale.value.trim(); + + const fallback = findField(modelFields, 'Fallback'); + if (fallback) result.modelFallback = fallback.value.trim(); + + const effort = findField(modelFields, 'Reasoning Effort'); + if (effort) { + const raw = effort.value.trim().toLowerCase(); + result.authoredReasoningEffort = raw; + // Normalize: "auto" → undefined, invalid values → undefined + if (raw !== 'auto' && VALID_EFFORTS.has(raw)) { + result.reasoningEffort = raw; + } else if (raw !== 'auto') { + // Surface invalid charter input to the author instead of dropping it silently. + console.warn( + `[squad] charter parse: ignoring invalid reasoning effort "${raw}" ` + + `(expected ${VALID_REASONING_EFFORTS.join(', ')}, or auto)`, + ); } } - - // Extract ## Collaboration section - const collaborationMatch = content.match(/##\s+Collaboration\s*\n([\s\S]*?)(?=\n##|\n---|$)/i); - if (collaborationMatch) { - result.collaboration = collaborationMatch[1]!.trim(); + + const tier = findField(modelFields, 'Context Tier'); + if (tier) { + const raw = tier.value.trim().toLowerCase(); + result.authoredContextTier = raw; + // Normalize: "auto" → undefined, invalid values → undefined + if (raw !== 'auto' && VALID_TIERS.has(raw)) { + result.contextTier = raw; + } else if (raw !== 'auto') { + // Surface invalid charter input to the author instead of dropping it silently. + console.warn( + `[squad] charter parse: ignoring invalid context tier "${raw}" ` + + `(expected ${VALID_CONTEXT_TIERS.join(', ')}, or auto)`, + ); + } } + // Extract canonical collaboration section or its legacy alias. + result.collaboration = findSectionBody( + scan, + 'Collaboration and Review Authority', + 'Collaboration', + ); + + Object.defineProperty(result, 'source', { + configurable: false, + enumerable: false, + writable: false, + value: { + semanticKey: charterSemanticKey(result), + canonicalShape: hasCanonicalShape(scan), + extensions: scan.sections + .filter(section => section.isExtension) + .map(section => section.raw), + } satisfies CharterSourceMetadata, + }); + return result; } +/** @internal Stable comparison key for source-aware serialization. */ +export function charterSemanticKey(charter: ParsedCharter): string { + return JSON.stringify({ + identity: charter.identity, + ownership: charter.ownership, + boundaries: charter.boundaries, + modelPreference: charter.modelPreference, + modelRationale: charter.modelRationale, + modelFallback: charter.modelFallback, + reasoningEffort: charter.reasoningEffort, + authoredReasoningEffort: charter.authoredReasoningEffort, + contextTier: charter.contextTier, + authoredContextTier: charter.authoredContextTier, + collaboration: charter.collaboration, + }); +} + +function fieldsInSection( + scan: CharterMarkdownScan, + sectionName: string, +): readonly CharterMarkdownField[] { + return scan.fields.filter( + field => field.section.toLowerCase() === sectionName.toLowerCase(), + ); +} + +function findField( + fields: readonly CharterMarkdownField[], + name: string, +): CharterMarkdownField | undefined { + return fields.find(field => field.name.toLowerCase() === name.toLowerCase()); +} + +function findSectionBody( + scan: CharterMarkdownScan, + ...names: readonly string[] +): string | undefined { + const section = scan.sections.find(candidate => + names.some(name => candidate.name.toLowerCase() === name.toLowerCase()), + ); + return section?.body; +} + +function stripCode(value: string): string { + const trimmed = value.trim(); + return trimmed.startsWith('`') && trimmed.endsWith('`') + ? trimmed.slice(1, -1) + : trimmed; +} + +function hasCanonicalShape(scan: CharterMarkdownScan): boolean { + const names = new Set(scan.sections.map(section => section.name.toLowerCase())); + return names.has('identity') + && names.has('accountable responsibilities') + && names.has('non-responsibilities') + && names.has('collaboration and review authority') + && !names.has('what i own') + && !names.has('boundaries') + && !names.has('collaboration'); +} + /** * Capitalize first letter of a string. */ diff --git a/packages/squad-sdk/src/agents/charter-markdown.ts b/packages/squad-sdk/src/agents/charter-markdown.ts new file mode 100644 index 000000000..cbde4cab9 --- /dev/null +++ b/packages/squad-sdk/src/agents/charter-markdown.ts @@ -0,0 +1,225 @@ +/** + * Deterministic Markdown scanner for the Squad Charter Profile. + * + * This is intentionally a small CommonMark-compatible subset rather than a + * general Markdown parser. Charter structure is recognized only at column one, + * outside fenced/indented code, HTML comments, block quotes, and opaque + * extension sections. + */ + +export interface CharterMarkdownHeading { + readonly name: string; + readonly line: number; + readonly column: number; + readonly start: number; + readonly isExtension: boolean; +} + +export interface CharterMarkdownField { + readonly name: string; + readonly value: string; + readonly line: number; + readonly column: number; + readonly section: string; +} + +export interface CharterMarkdownSection extends CharterMarkdownHeading { + readonly body: string; + readonly raw: string; + readonly end: number; +} + +export interface CharterMarkdownScan { + readonly markdown: string; + readonly eol: '\n' | '\r\n' | '\r'; + readonly h1?: CharterMarkdownHeading; + readonly sections: readonly CharterMarkdownSection[]; + readonly fields: readonly CharterMarkdownField[]; +} + +interface SourceLine { + readonly text: string; + readonly eol: string; + readonly line: number; + readonly start: number; + readonly end: number; +} + +interface PendingSection extends CharterMarkdownHeading { + readonly headingEnd: number; +} + +const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,}).*$/; +const BLOCK_QUOTE = /^ {0,3}>/; +const INDENTED_CODE = /^(?: {4}|\t)/; +const H1 = /^#(?!#)[ \t]+(.+?)[ \t]*#*[ \t]*$/; +const H2 = /^##(?!#)[ \t]+(.+?)[ \t]*#*[ \t]*$/; +const FIELD = /^ {0,3}(?:-[ \t]*)?\*\*([^*\r\n]+):\*\*[ \t]*(.*?)[ \t]*$/; +const EXTENSION_HEADING = /^x-/i; + +/** + * Scan charter Markdown using the profile's deterministic structural subset. + */ +export function scanCharterMarkdown(markdown: string): CharterMarkdownScan { + const lines = splitSourceLines(markdown); + const sections: CharterMarkdownSection[] = []; + const fields: CharterMarkdownField[] = []; + let h1: CharterMarkdownHeading | undefined; + let currentSection: PendingSection | undefined; + let extensionTail = false; + let fence: { marker: '`' | '~'; length: number } | undefined; + let htmlComment = false; + + const finishSection = (end: number): void => { + if (!currentSection) return; + const raw = markdown.slice(currentSection.start, end); + const body = markdown.slice(currentSection.headingEnd, end) + .replace(/^(?:\r\n|\r|\n)/, '') + .replace(/\r\n?|\n/g, '\n') + .trim(); + sections.push({ + ...currentSection, + body, + raw, + end, + }); + currentSection = undefined; + }; + + for (const sourceLine of lines) { + const { text } = sourceLine; + + if (htmlComment) { + if (text.includes('-->')) htmlComment = false; + continue; + } + + const trimmed = text.trimStart(); + if (trimmed.startsWith('')) htmlComment = true; + continue; + } + + if (fence) { + const close = text.match(/^ {0,3}(`{3,}|~{3,})[ \t]*$/); + if ( + close + && close[1]![0] === fence.marker + && close[1]!.length >= fence.length + ) { + fence = undefined; + } + continue; + } + + const fenceOpen = text.match(FENCE_OPEN); + if (fenceOpen) { + fence = { + marker: fenceOpen[1]![0] as '`' | '~', + length: fenceOpen[1]!.length, + }; + continue; + } + + if (BLOCK_QUOTE.test(text) || INDENTED_CODE.test(text)) continue; + + const h2Match = text.match(H2); + if (h2Match) { + const name = h2Match[1]!.trim(); + const isExtension = EXTENSION_HEADING.test(name); + + // The first extension heading begins an opaque tail. Within that tail, + // only another namespaced extension heading can delimit a new section. + if (!extensionTail || isExtension) { + finishSection(sourceLine.start); + currentSection = { + name, + line: sourceLine.line, + column: text.indexOf('##') + 1, + start: sourceLine.start, + headingEnd: sourceLine.end - sourceLine.eol.length, + isExtension, + }; + if (isExtension) extensionTail = true; + } + continue; + } + + if (extensionTail) continue; + + if (!h1) { + const h1Match = text.match(H1); + if (h1Match) { + h1 = { + name: h1Match[1]!.trim(), + line: sourceLine.line, + column: text.indexOf('#') + 1, + start: sourceLine.start, + isExtension: false, + }; + } + } + + if (!currentSection) continue; + const fieldMatch = text.match(FIELD); + if (fieldMatch) { + fields.push({ + name: fieldMatch[1]!.trim(), + value: fieldMatch[2]!, + line: sourceLine.line, + column: text.indexOf('**') + 1, + section: currentSection.name, + }); + } + } + + finishSection(markdown.length); + + return { + markdown, + eol: detectEol(markdown), + ...(h1 ? { h1 } : {}), + sections, + fields, + }; +} + +function splitSourceLines(markdown: string): SourceLine[] { + if (markdown.length === 0) { + return [{ text: '', eol: '', line: 1, start: 0, end: 0 }]; + } + + const lines: SourceLine[] = []; + let start = 0; + let line = 1; + while (start < markdown.length) { + let end = start; + while (end < markdown.length && markdown[end] !== '\r' && markdown[end] !== '\n') { + end += 1; + } + + let eol = ''; + if (markdown[end] === '\r' && markdown[end + 1] === '\n') { + eol = '\r\n'; + } else if (markdown[end] === '\r' || markdown[end] === '\n') { + eol = markdown[end]!; + } + + const next = end + eol.length; + lines.push({ + text: markdown.slice(start, end), + eol, + line, + start, + end: next, + }); + start = next; + line += 1; + } + return lines; +} + +function detectEol(markdown: string): '\n' | '\r\n' | '\r' { + const match = markdown.match(/\r\n|\r|\n/); + return (match?.[0] as '\n' | '\r\n' | '\r' | undefined) ?? '\n'; +} diff --git a/packages/squad-sdk/src/agents/charter-validator.ts b/packages/squad-sdk/src/agents/charter-validator.ts new file mode 100644 index 000000000..8f11b92ff --- /dev/null +++ b/packages/squad-sdk/src/agents/charter-validator.ts @@ -0,0 +1,469 @@ +import { VALID_CONTEXT_TIERS, VALID_REASONING_EFFORTS } from '../config/models.js'; +import { + scanCharterMarkdown, + type CharterMarkdownField, + type CharterMarkdownHeading, +} from './charter-markdown.js'; + +export const CHARTER_PROFILE = 'squad-charter/v0.1' as const; + +export const CHARTER_PROFILE_METADATA = { + id: CHARTER_PROFILE, + artifactClass: 'required', + rootRelativePath: 'agents/{id}/charter.md', + repositoryRelativePath: '.squad/agents/{id}/charter.md', +} as const; + +export const CHARTER_CAPABILITIES = { + parse: 'squad-charter/v0.1/parse', + validate: 'squad-charter/v0.1/validate', + edit: 'squad-charter/v0.1/edit', + legacyConsume: 'squad-charter/v0.1/legacy-consume', +} as const; + +export type CharterCapability = + typeof CHARTER_CAPABILITIES[keyof typeof CHARTER_CAPABILITIES]; + +export const CHARTER_DIAGNOSTIC_CODES = [ + 'SQC001', + 'SQC002', + 'SQC003', + 'SQC004', + 'SQC005', + 'SQC006', + 'SQC007', + 'SQC008', + 'SQC009', + 'SQC010', + 'SQC011', + 'SQC012', + 'SQC013', + 'SQC101', + 'SQC102', + 'SQC103', + 'SQC104', + 'SQC105', + 'SQC106', +] as const; + +export type CharterDiagnosticCode = typeof CHARTER_DIAGNOSTIC_CODES[number]; + +export const CHARTER_DIAGNOSTIC_SEVERITIES = ['error', 'warning'] as const; +export type CharterDiagnosticSeverity = typeof CHARTER_DIAGNOSTIC_SEVERITIES[number]; + +export type CharterConformance = + | 'canonical' + | 'noncanonical-compatible' + | 'invalid'; + +export interface CharterDiagnostic { + code: CharterDiagnosticCode; + severity: CharterDiagnosticSeverity; + message: string; + line: number; + column: number; + endLine: number; + endColumn: number; + path?: string; +} + +export interface CharterValidationOptions { + /** + * Artifact path context. Recognized forms are `agents/{id}/charter.md`, + * `.squad/agents/{id}/charter.md`, or an absolute path ending in the latter. + */ + path?: string; + /** Requested profile. Unsupported versions produce SQC013. */ + profile?: string; +} + +export interface CharterValidationResult { + profile: string; + /** True only for canonical profile documents. */ + conforms: boolean; + /** True when a consumer may accept the document, including legacy input. */ + accepted: boolean; + conformance: CharterConformance; + capabilities: readonly CharterCapability[]; + diagnostics: readonly CharterDiagnostic[]; +} + +interface FieldDefinition { + readonly name: string; + readonly section: 'Identity' | 'Model'; +} + +const ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; +const EXTENSION_PATTERN = /^X-[a-z0-9]+(?:-[a-z0-9]+)*-[a-z0-9]+(?:-[a-z0-9]+)*$/; +const CANONICAL_RESPONSIBILITY_SECTIONS = [ + 'Accountable Responsibilities', + 'Non-Responsibilities', + 'Collaboration and Review Authority', +] as const; +const LEGACY_RESPONSIBILITY_SECTIONS = ['What I Own', 'Boundaries', 'Collaboration'] as const; +const STANDARD_SECTION_GROUPS = [ + { semantic: 'Identity', names: ['Identity'] }, + { + semantic: 'Accountable Responsibilities', + names: ['Accountable Responsibilities', 'What I Own'], + }, + { + semantic: 'Non-Responsibilities', + names: ['Non-Responsibilities', 'Boundaries'], + }, + { + semantic: 'Collaboration and Review Authority', + names: ['Collaboration and Review Authority', 'Collaboration'], + }, + { semantic: 'Model', names: ['Model'] }, +] as const; +const UNIQUE_MACHINE_FIELDS: readonly FieldDefinition[] = [ + { name: 'ID', section: 'Identity' }, + { name: 'Purpose', section: 'Identity' }, + { name: 'Name', section: 'Identity' }, + { name: 'Role', section: 'Identity' }, + { name: 'Expertise', section: 'Identity' }, + { name: 'Style', section: 'Identity' }, + { name: 'Preferred', section: 'Model' }, + { name: 'Rationale', section: 'Model' }, + { name: 'Fallback', section: 'Model' }, + { name: 'Reasoning Effort', section: 'Model' }, + { name: 'Context Tier', section: 'Model' }, +] as const; +const VALID_EFFORTS = new Set(VALID_REASONING_EFFORTS); +const VALID_TIERS = new Set(VALID_CONTEXT_TIERS); +const DIAGNOSTIC_ORDER = new Map( + CHARTER_DIAGNOSTIC_CODES.map((code, index) => [code, index]), +); + +/** + * Validate a charter against the Squad Charter Profile v0.1. + * + * The validator is intentionally non-throwing so editors and migration tools + * can report every deterministic diagnostic in one pass. + */ +export function validateCharterMarkdown( + markdown: string, + options: CharterValidationOptions = {}, +): CharterValidationResult { + const requestedProfile = options.profile ?? CHARTER_PROFILE; + const diagnostics: CharterDiagnostic[] = []; + const add = ( + code: CharterDiagnosticCode, + severity: CharterDiagnosticSeverity, + message: string, + location: { line: number; column?: number; width?: number }, + ): void => { + const column = location.column ?? 1; + const width = Math.max(location.width ?? 1, 1); + diagnostics.push({ + code, + severity, + message, + line: location.line, + column, + endLine: location.line, + endColumn: column + width - 1, + ...(options.path ? { path: options.path } : {}), + }); + }; + + if (requestedProfile !== CHARTER_PROFILE) { + add( + 'SQC013', + 'error', + `unsupported charter profile "${requestedProfile}"`, + { line: 1 }, + ); + return createResult(requestedProfile, diagnostics, []); + } + + const scan = scanCharterMarkdown(markdown); + if (!markdown.trim()) { + add('SQC001', 'error', 'charter.md must not be empty', { line: 1 }); + return createResult(requestedProfile, diagnostics); + } + + if (!scan.h1) { + add('SQC002', 'error', 'charter.md must contain an H1 identity heading', { line: 1 }); + } else if (!/^.+?\s+(?:—|–|-)\s+.+?$/.test(scan.h1.name)) { + add( + 'SQC003', + 'warning', + 'the canonical H1 form is "# — "', + headingLocation(scan.h1), + ); + } + + const identityHeadings = headingsNamed(scan.sections, 'Identity'); + const identityHeading = identityHeadings[0]; + if (!identityHeading) { + add( + 'SQC101', + 'warning', + 'canonical charters include an "Identity" section', + { line: scan.h1?.line ?? 1 }, + ); + } + + for (const group of STANDARD_SECTION_GROUPS) { + const matches = scan.sections.filter(section => + group.names.some(name => equalName(section.name, name)), + ); + for (const duplicate of matches.slice(1)) { + add( + 'SQC011', + 'error', + `standard section "${group.semantic}" must appear at most once`, + headingLocation(duplicate), + ); + } + } + + for (const fieldDefinition of UNIQUE_MACHINE_FIELDS) { + const matches = findFields(scan.fields, fieldDefinition.name, fieldDefinition.section); + for (const duplicate of matches.slice(1)) { + add( + 'SQC004', + 'error', + `machine-readable field "${fieldDefinition.name}" must appear at most once`, + fieldLocation(duplicate), + ); + } + } + + const id = findField(scan.fields, 'ID', 'Identity'); + const legacyName = findField(scan.fields, 'Name', 'Identity'); + if (id) { + const normalizedId = stripCode(id.value); + if (!ID_PATTERN.test(normalizedId)) { + add( + 'SQC005', + 'error', + 'Identity ID must be a lowercase kebab-case identifier', + fieldLocation(id), + ); + } + + const pathId = profileIdentityFromPath(options.path); + if (pathId !== undefined && pathId !== normalizedId) { + add( + 'SQC010', + 'error', + `Identity ID "${normalizedId}" does not match profile path ID "${pathId}"`, + fieldLocation(id), + ); + } + } else { + add( + 'SQC102', + 'warning', + legacyName + ? 'legacy Identity Name is accepted, but canonical charters declare Identity ID' + : 'canonical charters declare a non-empty Identity ID', + { line: identityHeading?.line ?? scan.h1?.line ?? 1 }, + ); + } + + const purpose = findField(scan.fields, 'Purpose', 'Identity'); + if (!purpose) { + add( + 'SQC103', + 'warning', + 'canonical charters declare a non-empty Identity Purpose', + { line: identityHeading?.line ?? scan.h1?.line ?? 1 }, + ); + } else if (!purpose.value.trim()) { + add( + 'SQC009', + 'error', + 'Identity Purpose must be non-empty', + fieldLocation(purpose), + ); + } + + const headingNames = new Set(scan.sections.map(section => section.name.toLowerCase())); + for (const section of CANONICAL_RESPONSIBILITY_SECTIONS) { + if (!headingNames.has(section.toLowerCase())) { + add( + 'SQC104', + 'warning', + `canonical charters include a "${section}" section`, + { line: scan.h1?.line ?? 1 }, + ); + } + } + + const firstLegacy = scan.sections.find(heading => + LEGACY_RESPONSIBILITY_SECTIONS.some(section => equalName(section, heading.name)), + ); + if (firstLegacy) { + add( + 'SQC105', + 'warning', + 'legacy responsibility headings are consumer-compatible but are not canonical v0.1', + headingLocation(firstLegacy), + ); + } + + const preferred = findField(scan.fields, 'Preferred', 'Model'); + if (preferred && !preferred.value.trim()) { + add( + 'SQC006', + 'error', + 'Model Preferred must be non-empty when present', + fieldLocation(preferred), + ); + } + + const reasoningEffort = findField(scan.fields, 'Reasoning Effort', 'Model'); + if (reasoningEffort) { + const value = reasoningEffort.value.trim().toLowerCase(); + if (value !== 'auto' && !VALID_EFFORTS.has(value)) { + add( + 'SQC007', + 'error', + `Reasoning Effort must be one of: auto, ${VALID_REASONING_EFFORTS.join(', ')}`, + fieldLocation(reasoningEffort), + ); + } + } + + const contextTier = findField(scan.fields, 'Context Tier', 'Model'); + if (contextTier) { + const value = contextTier.value.trim().toLowerCase(); + if (value !== 'auto' && !VALID_TIERS.has(value)) { + add( + 'SQC008', + 'error', + `Context Tier must be one of: auto, ${VALID_CONTEXT_TIERS.join(', ')}`, + fieldLocation(contextTier), + ); + } + } + + const extensions = scan.sections.filter(section => section.isExtension); + const extensionNames = new Set(); + for (const extension of extensions) { + const normalizedName = extension.name.toLowerCase(); + if (extensionNames.has(normalizedName)) { + add( + 'SQC012', + 'error', + `extension section "${extension.name}" collides case-insensitively with an earlier extension`, + headingLocation(extension), + ); + } + extensionNames.add(normalizedName); + + if (!EXTENSION_PATTERN.test(extension.name)) { + add( + 'SQC106', + 'warning', + 'extension headings should use "X--"', + headingLocation(extension), + ); + } + } + + diagnostics.sort((left, right) => + left.line - right.line + || left.column - right.column + || diagnosticIndex(left.code) - diagnosticIndex(right.code), + ); + + return createResult(requestedProfile, diagnostics); +} + +function createResult( + profile: string, + diagnostics: readonly CharterDiagnostic[], + capabilities: readonly CharterCapability[] = [CHARTER_CAPABILITIES.validate], +): CharterValidationResult { + const accepted = diagnostics.every(diagnostic => diagnostic.severity !== 'error'); + const conformance: CharterConformance = !accepted + ? 'invalid' + : diagnostics.length === 0 + ? 'canonical' + : 'noncanonical-compatible'; + return { + profile, + conforms: conformance === 'canonical', + accepted, + conformance, + capabilities, + diagnostics, + }; +} + +function findFields( + fields: readonly CharterMarkdownField[], + name: string, + section: string, +): CharterMarkdownField[] { + return fields.filter(field => + equalName(field.name, name) && equalName(field.section, section), + ); +} + +function findField( + fields: readonly CharterMarkdownField[], + name: string, + section: string, +): CharterMarkdownField | undefined { + return findFields(fields, name, section)[0]; +} + +function headingsNamed( + headings: readonly CharterMarkdownHeading[], + name: string, +): CharterMarkdownHeading[] { + return headings.filter(heading => equalName(heading.name, name)); +} + +function equalName(left: string, right: string): boolean { + return left.toLowerCase() === right.toLowerCase(); +} + +function stripCode(value: string): string { + const trimmed = value.trim(); + return trimmed.startsWith('`') && trimmed.endsWith('`') + ? trimmed.slice(1, -1) + : trimmed; +} + +function profileIdentityFromPath(path: string | undefined): string | undefined { + if (!path) return undefined; + const normalized = path.replace(/\\/g, '/').replace(/\/+/g, '/'); + const artifactRelative = normalized.match(/^agents\/([^/]+)\/charter\.md$/); + if (artifactRelative) return artifactRelative[1]; + + const squadRelative = normalized.match( + /(?:^|\/)\.squad\/agents\/([^/]+)\/charter\.md$/, + ); + return squadRelative?.[1]; +} + +function headingLocation( + heading: CharterMarkdownHeading, +): { line: number; column: number; width: number } { + return { + line: heading.line, + column: heading.column, + width: heading.name.length + 3, + }; +} + +function fieldLocation( + field: CharterMarkdownField, +): { line: number; column: number; width: number } { + return { + line: field.line, + column: field.column, + width: field.name.length + 5, + }; +} + +function diagnosticIndex(code: CharterDiagnosticCode): number { + return DIAGNOSTIC_ORDER.get(code) ?? Number.MAX_SAFE_INTEGER; +} diff --git a/packages/squad-sdk/src/agents/index.ts b/packages/squad-sdk/src/agents/index.ts index 079f75eca..0d2141400 100644 --- a/packages/squad-sdk/src/agents/index.ts +++ b/packages/squad-sdk/src/agents/index.ts @@ -38,9 +38,26 @@ export { type CharterCompileOptions, type CharterConfigOverrides, type ParsedCharter, + type CharterSourceMetadata, type CompiledCharter, } from './charter-compiler.js'; +export { + CHARTER_CAPABILITIES, + CHARTER_DIAGNOSTIC_CODES, + CHARTER_DIAGNOSTIC_SEVERITIES, + CHARTER_PROFILE, + CHARTER_PROFILE_METADATA, + validateCharterMarkdown, + type CharterCapability, + type CharterConformance, + type CharterDiagnostic, + type CharterDiagnosticCode, + type CharterDiagnosticSeverity, + type CharterValidationOptions, + type CharterValidationResult, +} from './charter-validator.js'; + // --- M1-9 Model Selection + M3-5 Model Fallback --- export { resolveModel, diff --git a/packages/squad-sdk/src/parsers.ts b/packages/squad-sdk/src/parsers.ts index c931674f7..826afa112 100644 --- a/packages/squad-sdk/src/parsers.ts +++ b/packages/squad-sdk/src/parsers.ts @@ -45,13 +45,42 @@ export { compileCharterFull, } from './agents/charter-compiler.js'; +export { + CHARTER_CAPABILITIES, + CHARTER_DIAGNOSTIC_CODES, + CHARTER_DIAGNOSTIC_SEVERITIES, + CHARTER_PROFILE, + CHARTER_PROFILE_METADATA, + validateCharterMarkdown, +} from './agents/charter-validator.js'; + +export type { + CharterCapability, + CharterConformance, + CharterDiagnostic, + CharterDiagnosticCode, + CharterDiagnosticSeverity, + CharterValidationOptions, + CharterValidationResult, +} from './agents/charter-validator.js'; + export type { ParsedCharter, CharterCompileOptions, CharterConfigOverrides, + CharterSourceMetadata, CompiledCharter, } from './agents/charter-compiler.js'; +export { + parseCharter, + serializeCharter, +} from './state/io/charter-io.js'; + +export type { + CharterSerializeOptions, +} from './state/io/charter-io.js'; + // --- skills/skill-loader.ts --- export { parseFrontmatter, diff --git a/packages/squad-sdk/src/state/io/charter-io.ts b/packages/squad-sdk/src/state/io/charter-io.ts index 82a14566d..5d142d56d 100644 --- a/packages/squad-sdk/src/state/io/charter-io.ts +++ b/packages/squad-sdk/src/state/io/charter-io.ts @@ -7,10 +7,23 @@ * @module state/io/charter-io */ -import { parseCharterMarkdown, type ParsedCharter } from '../../agents/charter-compiler.js'; +import { + charterSemanticKey, + parseCharterMarkdown, + type ParsedCharter, +} from '../../agents/charter-compiler.js'; export type { ParsedCharter }; +export interface CharterSerializeOptions { + /** + * Canonical output is the default. `legacy` explicitly emits legacy + * responsibility headings. `preserve` returns the original source verbatim + * when source metadata is available. + */ + format?: 'canonical' | 'legacy' | 'preserve'; +} + /** * Parse charter markdown into a typed structure. * Delegates to the existing `parseCharterMarkdown()`. @@ -30,7 +43,23 @@ export function parseCharter(markdown: string): ParsedCharter { * ... * ``` */ -export function serializeCharter(charter: ParsedCharter): string { +export function serializeCharter( + charter: ParsedCharter, + options: CharterSerializeOptions = {}, +): string { + const format = options.format ?? 'canonical'; + if (format === 'preserve' && charter.fullContent) { + return charter.fullContent; + } + + if ( + format === 'canonical' + && charter.source?.canonicalShape + && charter.source.semanticKey === charterSemanticKey(charter) + ) { + return charter.fullContent; + } + const lines: string[] = []; // Title line @@ -48,10 +77,16 @@ export function serializeCharter(charter: ParsedCharter): string { // Identity section lines.push('## Identity'); lines.push(''); - if (charter.identity.name) { + if (charter.identity.id) { + lines.push(`- **ID:** \`${charter.identity.id}\``); + } + if (charter.identity.purpose) { + lines.push(`- **Purpose:** ${charter.identity.purpose}`); + } + if (!charter.identity.id && charter.identity.name) { lines.push(`- **Name:** ${charter.identity.name}`); } - if (charter.identity.role) { + if (!charter.identity.purpose && charter.identity.role) { lines.push(`- **Role:** ${charter.identity.role}`); } if (charter.identity.expertise && charter.identity.expertise.length > 0) { @@ -62,24 +97,44 @@ export function serializeCharter(charter: ParsedCharter): string { } lines.push(''); - // What I Own section + const headings = format === 'legacy' + ? { + ownership: 'What I Own', + boundaries: 'Boundaries', + collaboration: 'Collaboration', + } + : { + ownership: 'Accountable Responsibilities', + boundaries: 'Non-Responsibilities', + collaboration: 'Collaboration and Review Authority', + }; + + // Accountable responsibilities if (charter.ownership !== undefined) { - lines.push('## What I Own'); + lines.push(`## ${headings.ownership}`); lines.push(''); lines.push(charter.ownership); lines.push(''); } - // Boundaries section + // Non-responsibilities if (charter.boundaries !== undefined) { - lines.push('## Boundaries'); + lines.push(`## ${headings.boundaries}`); lines.push(''); lines.push(charter.boundaries); lines.push(''); } // Model section - if (charter.modelPreference || charter.modelRationale || charter.modelFallback) { + if ( + charter.modelPreference + || charter.modelRationale + || charter.modelFallback + || charter.reasoningEffort + || charter.authoredReasoningEffort + || charter.contextTier + || charter.authoredContextTier + ) { lines.push('## Model'); lines.push(''); if (charter.modelPreference) { @@ -91,16 +146,28 @@ export function serializeCharter(charter: ParsedCharter): string { if (charter.modelFallback) { lines.push(`**Fallback:** ${charter.modelFallback}`); } + const effort = charter.reasoningEffort ?? charter.authoredReasoningEffort; + if (effort) { + lines.push(`**Reasoning Effort:** ${effort}`); + } + const tier = charter.contextTier ?? charter.authoredContextTier; + if (tier) { + lines.push(`**Context Tier:** ${tier}`); + } lines.push(''); } // Collaboration section if (charter.collaboration !== undefined) { - lines.push('## Collaboration'); + lines.push(`## ${headings.collaboration}`); lines.push(''); lines.push(charter.collaboration); lines.push(''); } - return lines.join('\n').trimEnd() + '\n'; + const canonical = lines.join('\n').trimEnd() + '\n'; + const extensions = charter.source?.extensions.join('') ?? ''; + return extensions + ? `${canonical.trimEnd()}\n\n${extensions}` + : canonical; } diff --git a/packages/squad-sdk/src/types.ts b/packages/squad-sdk/src/types.ts index 2c5682656..e5fe0dda8 100644 --- a/packages/squad-sdk/src/types.ts +++ b/packages/squad-sdk/src/types.ts @@ -17,7 +17,16 @@ export type { MarkdownMigrationResult } from './config/markdown-migration.js'; export type { ParsedCharter } from './agents/charter-compiler.js'; export type { CharterCompileOptions } from './agents/charter-compiler.js'; export type { CharterConfigOverrides } from './agents/charter-compiler.js'; +export type { CharterSourceMetadata } from './agents/charter-compiler.js'; export type { CompiledCharter } from './agents/charter-compiler.js'; +export type { CharterCapability } from './agents/charter-validator.js'; +export type { CharterConformance } from './agents/charter-validator.js'; +export type { CharterDiagnostic } from './agents/charter-validator.js'; +export type { CharterDiagnosticCode } from './agents/charter-validator.js'; +export type { CharterDiagnosticSeverity } from './agents/charter-validator.js'; +export type { CharterValidationOptions } from './agents/charter-validator.js'; +export type { CharterValidationResult } from './agents/charter-validator.js'; +export type { CharterSerializeOptions } from './state/io/charter-io.js'; // --- Routing types (config/routing.ts) --- export type { CompiledRouter } from './config/routing.js'; diff --git a/test-fixtures/spec/charter-v0.1/legacy/template-style.md b/test-fixtures/spec/charter-v0.1/legacy/template-style.md new file mode 100644 index 000000000..5b038fec3 --- /dev/null +++ b/test-fixtures/spec/charter-v0.1/legacy/template-style.md @@ -0,0 +1,21 @@ +# Legacy Agent — Developer + +## Identity +- **Name:** Legacy Agent +- **Role:** Developer +- **Expertise:** TypeScript, testing +- **Style:** Direct + +## What I Own +- Legacy compatibility. + +## Boundaries +- Does not own architecture. + +## Collaboration +- Works with the lead. + +## Model +- **Preferred:** auto +- **Reasoning Effort:** auto +- **Context Tier:** auto diff --git a/test-fixtures/spec/charter-v0.1/manifest.json b/test-fixtures/spec/charter-v0.1/manifest.json new file mode 100644 index 000000000..c85359dc1 --- /dev/null +++ b/test-fixtures/spec/charter-v0.1/manifest.json @@ -0,0 +1,87 @@ +{ + "profile": "squad-charter/v0.1", + "requiredCapabilities": [ + "squad-charter/v0.1/parse", + "squad-charter/v0.1/validate", + "squad-charter/v0.1/edit", + "squad-charter/v0.1/legacy-consume" + ], + "fixtures": [ + { + "path": "positive/canonical.md", + "expected": { + "accepted": true, + "conforms": true, + "conformance": "canonical", + "diagnostics": [], + "identityId": "architect", + "reasoningEffort": "high", + "contextTier": "long_context" + } + }, + { + "path": "negative/invalid-behavior.md", + "expected": { + "accepted": false, + "conforms": false, + "conformance": "invalid", + "diagnostics": [ + { "code": "SQC005", "severity": "error", "line": 4, "column": 3 }, + { "code": "SQC007", "severity": "error", "line": 17, "column": 3 }, + { "code": "SQC004", "severity": "error", "line": 18, "column": 3 }, + { "code": "SQC008", "severity": "error", "line": 19, "column": 3 } + ] + } + }, + { + "path": "legacy/template-style.md", + "expected": { + "accepted": true, + "conforms": false, + "conformance": "noncanonical-compatible", + "diagnostics": [ + { "code": "SQC104", "severity": "warning", "line": 1, "column": 1 }, + { "code": "SQC104", "severity": "warning", "line": 1, "column": 1 }, + { "code": "SQC104", "severity": "warning", "line": 1, "column": 1 }, + { "code": "SQC102", "severity": "warning", "line": 3, "column": 1 }, + { "code": "SQC103", "severity": "warning", "line": 3, "column": 1 }, + { "code": "SQC105", "severity": "warning", "line": 9, "column": 1 } + ], + "identityName": "Legacy Agent", + "identityRole": "Developer" + } + }, + { + "path": "round-trip/extensions.md", + "expected": { + "accepted": true, + "conforms": true, + "conformance": "canonical", + "diagnostics": [], + "identityId": "extensible-agent", + "serializedOutputPath": "round-trip/extensions.md", + "editedOutputPath": "round-trip/extensions.edited.md" + } + } + ], + "duplicateFields": [ + { "section": "Identity", "name": "ID", "value": "`duplicate-agent`" }, + { "section": "Identity", "name": "Purpose", "value": "Duplicate purpose." }, + { "section": "Identity", "name": "Name", "value": "Duplicate Agent" }, + { "section": "Identity", "name": "Role", "value": "Tester" }, + { "section": "Identity", "name": "Expertise", "value": "testing" }, + { "section": "Identity", "name": "Style", "value": "Direct" }, + { "section": "Model", "name": "Preferred", "value": "auto" }, + { "section": "Model", "name": "Rationale", "value": "Testing." }, + { "section": "Model", "name": "Fallback", "value": "fallback-model" }, + { "section": "Model", "name": "Reasoning Effort", "value": "auto" }, + { "section": "Model", "name": "Context Tier", "value": "auto" } + ], + "duplicateSections": [ + "Identity", + "Accountable Responsibilities", + "Non-Responsibilities", + "Collaboration and Review Authority", + "Model" + ] +} diff --git a/test-fixtures/spec/charter-v0.1/negative/invalid-behavior.md b/test-fixtures/spec/charter-v0.1/negative/invalid-behavior.md new file mode 100644 index 000000000..508c9ea1a --- /dev/null +++ b/test-fixtures/spec/charter-v0.1/negative/invalid-behavior.md @@ -0,0 +1,19 @@ +# Invalid Agent — Tester + +## Identity +- **ID:** `Invalid Agent` +- **Purpose:** Exercise deterministic diagnostics. + +## Accountable Responsibilities +- Negative fixture. + +## Non-Responsibilities +- Production use. + +## Collaboration and Review Authority +- None. + +## Model +- **Reasoning Effort:** turbo +- **Reasoning Effort:** low +- **Context Tier:** enormous diff --git a/test-fixtures/spec/charter-v0.1/positive/canonical.md b/test-fixtures/spec/charter-v0.1/positive/canonical.md new file mode 100644 index 000000000..ce7f80615 --- /dev/null +++ b/test-fixtures/spec/charter-v0.1/positive/canonical.md @@ -0,0 +1,19 @@ +# Architect — Lead Architect + +## Identity +- **ID:** `architect` +- **Purpose:** Keep cross-package architecture coherent. + +## Accountable Responsibilities +- Cross-package architecture and compatibility strategy. + +## Non-Responsibilities +- Routine implementation owned by domain specialists. + +## Collaboration and Review Authority +- Adjudicates compatibility disputes. + +## Model +- **Preferred:** auto +- **Reasoning Effort:** high +- **Context Tier:** long_context diff --git a/test-fixtures/spec/charter-v0.1/round-trip/extensions.edited.md b/test-fixtures/spec/charter-v0.1/round-trip/extensions.edited.md new file mode 100644 index 000000000..a55468343 --- /dev/null +++ b/test-fixtures/spec/charter-v0.1/round-trip/extensions.edited.md @@ -0,0 +1,54 @@ +# Extensible Agent — Integrator + +## Identity + +- **ID:** `extensible-agent` +- **Purpose:** Verify edited fields without losing unknown extensions. + +## Accountable Responsibilities + +- Integration behavior. + +## Non-Responsibilities + +- Extension interpretation. + +## Collaboration and Review Authority + +- Coordinates with extension owners. + +## X-example-deployment + +opaque: keep-this-value + +```yaml +targets: + - staging +``` + +```markdown +## Model +- **Reasoning Effort:** turbo +``` + + + +> ## Model +> - **Context Tier:** enormous + + + + ## Identity + - **ID:** indented-code + + + +## Identity +- **ID:** extension-body-text + +## X-example-policy + +preserve: this-second-extension diff --git a/test-fixtures/spec/charter-v0.1/round-trip/extensions.md b/test-fixtures/spec/charter-v0.1/round-trip/extensions.md new file mode 100644 index 000000000..13ef93ae4 --- /dev/null +++ b/test-fixtures/spec/charter-v0.1/round-trip/extensions.md @@ -0,0 +1,50 @@ +# Extensible Agent — Integrator + +## Identity +- **ID:** `extensible-agent` +- **Purpose:** Verify unknown extension preservation requirements. + +## Accountable Responsibilities +- Integration behavior. + +## Non-Responsibilities +- Extension interpretation. + +## Collaboration and Review Authority +- Coordinates with extension owners. + +## X-example-deployment + +opaque: keep-this-value + +```yaml +targets: + - staging +``` + +```markdown +## Model +- **Reasoning Effort:** turbo +``` + + + +> ## Model +> - **Context Tier:** enormous + + + + ## Identity + - **ID:** indented-code + + + +## Identity +- **ID:** extension-body-text + +## X-example-policy + +preserve: this-second-extension diff --git a/test/charter-spec.test.ts b/test/charter-spec.test.ts new file mode 100644 index 000000000..510f36385 --- /dev/null +++ b/test/charter-spec.test.ts @@ -0,0 +1,397 @@ +import { readFile } from 'node:fs/promises'; +import * as path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { + CHARTER_CAPABILITIES, + CHARTER_PROFILE, + CHARTER_PROFILE_METADATA, + compileCharterFull, + parseCharterMarkdown, + serializeCharter, + validateCharterMarkdown, + type CharterConformance, + type CharterDiagnosticCode, + type CharterDiagnosticSeverity, +} from '@bradygaster/squad-sdk/parsers'; + +interface ExpectedDiagnostic { + code: CharterDiagnosticCode; + severity: CharterDiagnosticSeverity; + line: number; + column: number; +} + +interface FixtureManifest { + profile: string; + requiredCapabilities: string[]; + fixtures: Array<{ + path: string; + expected: { + accepted: boolean; + conforms: boolean; + conformance: CharterConformance; + diagnostics: ExpectedDiagnostic[]; + identityId?: string; + identityName?: string; + identityRole?: string; + reasoningEffort?: string; + contextTier?: string; + serializedOutputPath?: string; + editedOutputPath?: string; + }; + }>; + duplicateFields: Array<{ + section: 'Identity' | 'Model'; + name: string; + value: string; + }>; + duplicateSections: string[]; +} + +const FIXTURE_ROOT = path.resolve(__dirname, '..', 'test-fixtures', 'spec', 'charter-v0.1'); + +async function loadManifest(): Promise { + return JSON.parse( + await readFile(path.join(FIXTURE_ROOT, 'manifest.json'), 'utf8'), + ) as FixtureManifest; +} + +const CANONICAL = `# Contract Agent — Tester + +## Identity +- **ID:** \`contract-agent\` +- **Purpose:** Exercise the charter contract. + +## Accountable Responsibilities +- Contract tests. + +## Non-Responsibilities +- Runtime authorization. + +## Collaboration and Review Authority +- Reviews public API compatibility. + +## Model +- **Preferred:** auto +- **Reasoning Effort:** auto +- **Context Tier:** auto +`; + +describe('Squad Charter Profile v0.1', () => { + it('executes the language-neutral conformance manifest', async () => { + const manifest = await loadManifest(); + + expect(manifest.profile).toBe(CHARTER_PROFILE); + expect(manifest.requiredCapabilities).toEqual(Object.values(CHARTER_CAPABILITIES)); + expect(CHARTER_PROFILE_METADATA).toEqual({ + id: CHARTER_PROFILE, + artifactClass: 'required', + rootRelativePath: 'agents/{id}/charter.md', + repositoryRelativePath: '.squad/agents/{id}/charter.md', + }); + + for (const fixture of manifest.fixtures) { + const fixturePath = path.join(FIXTURE_ROOT, fixture.path); + const markdown = await readFile(fixturePath, 'utf8'); + const validation = validateCharterMarkdown(markdown, { path: fixture.path }); + const parsed = parseCharterMarkdown(markdown); + + expect(validation.accepted, fixture.path).toBe(fixture.expected.accepted); + expect(validation.conforms, fixture.path).toBe(fixture.expected.conforms); + expect(validation.conformance, fixture.path).toBe(fixture.expected.conformance); + expect(validation.capabilities, fixture.path).toEqual([CHARTER_CAPABILITIES.validate]); + expect( + validation.diagnostics.map(({ code, severity, line, column }) => ({ + code, + severity, + line, + column, + })), + fixture.path, + ).toEqual(fixture.expected.diagnostics); + + if (fixture.expected.identityId) { + expect(parsed.identity.id, fixture.path).toBe(fixture.expected.identityId); + } + if (fixture.expected.identityName) { + expect(parsed.identity.name, fixture.path).toBe(fixture.expected.identityName); + } + if (fixture.expected.identityRole) { + expect(parsed.identity.role, fixture.path).toBe(fixture.expected.identityRole); + } + if (fixture.expected.reasoningEffort) { + expect(parsed.reasoningEffort, fixture.path).toBe(fixture.expected.reasoningEffort); + } + if (fixture.expected.contextTier) { + expect(parsed.contextTier, fixture.path).toBe(fixture.expected.contextTier); + } + } + }); + + it('preserves exact source on canonical no-op and exact extensions after a known edit', async () => { + const manifest = await loadManifest(); + const fixture = manifest.fixtures.find(item => item.expected.editedOutputPath); + expect(fixture).toBeDefined(); + + const markdown = await readFile(path.join(FIXTURE_ROOT, fixture!.path), 'utf8'); + const expectedNoOp = await readFile( + path.join(FIXTURE_ROOT, fixture!.expected.serializedOutputPath!), + 'utf8', + ); + const expectedEdited = await readFile( + path.join(FIXTURE_ROOT, fixture!.expected.editedOutputPath!), + 'utf8', + ); + const parsed = parseCharterMarkdown(markdown); + + expect(serializeCharter(parsed)).toBe(expectedNoOp); + + parsed.identity.purpose = 'Verify edited fields without losing unknown extensions.'; + const edited = serializeCharter(parsed); + expect(edited).toBe(expectedEdited); + expect(edited.indexOf('## X-example-deployment')) + .toBeLessThan(edited.indexOf('## X-example-policy')); + }); + + it('uses one scanner that ignores structural lookalikes in opaque contexts', async () => { + const markdown = await readFile( + path.join(FIXTURE_ROOT, 'round-trip', 'extensions.md'), + 'utf8', + ); + const parsed = parseCharterMarkdown(markdown); + const validation = validateCharterMarkdown(markdown); + + expect(parsed.identity.id).toBe('extensible-agent'); + expect(parsed.modelPreference).toBeUndefined(); + expect(parsed.reasoningEffort).toBeUndefined(); + expect(parsed.contextTier).toBeUndefined(); + expect(validation).toMatchObject({ + accepted: true, + conforms: true, + conformance: 'canonical', + diagnostics: [], + }); + }); + + it('preserves CRLF source exactly and preserves CRLF extension bytes after editing', () => { + const markdown = CANONICAL.replace(/\n/g, '\r\n') + + '\r\n## X-example-data\r\n\r\nopaque: true\r\n'; + const parsed = parseCharterMarkdown(markdown); + expect(serializeCharter(parsed)).toBe(markdown); + + parsed.identity.purpose = 'Edited purpose.'; + const edited = serializeCharter(parsed); + expect(edited).toContain('## X-example-data\r\n\r\nopaque: true\r\n'); + expect(validateCharterMarkdown(edited).accepted).toBe(true); + }); + + it('enforces profile identity for POSIX and Windows paths only', () => { + expect( + validateCharterMarkdown(CANONICAL, { + path: '/repo/.squad/agents/contract-agent/charter.md', + }).diagnostics, + ).toEqual([]); + expect( + validateCharterMarkdown(CANONICAL, { + path: 'C:\\repo\\.squad\\agents\\contract-agent\\charter.md', + }).diagnostics, + ).toEqual([]); + expect( + validateCharterMarkdown(CANONICAL, { + path: 'agents/contract-agent/charter.md', + }).diagnostics, + ).toEqual([]); + + const mismatch = validateCharterMarkdown(CANONICAL, { + path: '.squad/agents/different-agent/charter.md', + }); + expect(mismatch.diagnostics.map(diagnostic => diagnostic.code)).toEqual(['SQC010']); + expect(mismatch.diagnostics[0]).toMatchObject({ line: 4, column: 3 }); + + expect( + validateCharterMarkdown(CANONICAL, { path: 'docs/charter.md' }).diagnostics, + ).toEqual([]); + }); + + it('rejects every duplicate standard machine field', async () => { + const manifest = await loadManifest(); + for (const duplicate of manifest.duplicateFields) { + const markdown = charterWithDuplicateField(duplicate); + const diagnostics = validateCharterMarkdown(markdown).diagnostics; + expect( + diagnostics.filter(diagnostic => diagnostic.code === 'SQC004'), + `${duplicate.section}.${duplicate.name}`, + ).toHaveLength(1); + } + }); + + it('rejects duplicate standard sections, including semantic legacy aliases', async () => { + const manifest = await loadManifest(); + for (const section of manifest.duplicateSections) { + const markdown = `${CANONICAL}\n## ${section}\n`; + expect( + validateCharterMarkdown(markdown).diagnostics.some( + diagnostic => diagnostic.code === 'SQC011', + ), + section, + ).toBe(true); + } + + const aliasDuplicate = CANONICAL.replace( + '## Non-Responsibilities', + '## Boundaries\n\nLegacy duplicate.\n\n## Non-Responsibilities', + ); + expect( + validateCharterMarkdown(aliasDuplicate).diagnostics.some( + diagnostic => diagnostic.code === 'SQC011', + ), + ).toBe(true); + }); + + function charterWithDuplicateField( + duplicate: FixtureManifest['duplicateFields'][number], + ): string { + const duplicateLines = [ + `- **${duplicate.name}:** ${duplicate.value}`, + `- **${duplicate.name}:** ${duplicate.value}`, + ].join('\n'); + const identityFields = duplicate.section === 'Identity' + ? [ + ...(duplicate.name === 'ID' ? [] : ['- **ID:** `contract-agent`']), + ...(duplicate.name === 'Purpose' ? [] : ['- **Purpose:** Exercise the charter contract.']), + duplicateLines, + ].join('\n') + : [ + '- **ID:** `contract-agent`', + '- **Purpose:** Exercise the charter contract.', + ].join('\n'); + const modelFields = duplicate.section === 'Model' + ? duplicateLines + : [ + '- **Preferred:** auto', + '- **Reasoning Effort:** auto', + '- **Context Tier:** auto', + ].join('\n'); + + return [ + '# Contract Agent — Tester', + '', + '## Identity', + identityFields, + '', + '## Accountable Responsibilities', + '- Contract tests.', + '', + '## Non-Responsibilities', + '- Runtime authorization.', + '', + '## Collaboration and Review Authority', + '- Reviews public API compatibility.', + '', + '## Model', + modelFields, + '', + ].join('\n'); + } + + it('emits canonical responsibility headings unless legacy mode is explicit', async () => { + const legacy = await readFile( + path.join(FIXTURE_ROOT, 'legacy', 'template-style.md'), + 'utf8', + ); + const parsed = parseCharterMarkdown(legacy); + const canonical = serializeCharter(parsed); + + expect(canonical).toContain('## Accountable Responsibilities'); + expect(canonical).toContain('## Non-Responsibilities'); + expect(canonical).toContain('## Collaboration and Review Authority'); + expect(canonical).not.toContain('## What I Own'); + expect(serializeCharter(parsed, { format: 'legacy' })).toContain('## What I Own'); + expect(serializeCharter(parsed, { format: 'preserve' })).toBe(legacy); + }); + + it('preserves authored auto preferences without producing runtime overrides', () => { + const compiled = compileCharterFull({ + agentName: 'contract-agent', + charterPath: '.squad/agents/contract-agent/charter.md', + charterContent: CANONICAL, + }); + + expect(compiled.parsed.modelPreference).toBe('auto'); + expect(compiled.parsed.authoredReasoningEffort).toBe('auto'); + expect(compiled.parsed.authoredContextTier).toBe('auto'); + expect(compiled.resolvedModel).toBeUndefined(); + expect(compiled.resolvedReasoningEffort).toBeUndefined(); + expect(compiled.resolvedContextTier).toBeUndefined(); + expect(serializeCharter(compiled.parsed)).toBe(CANONICAL); + + const explicitAutoOverride = compileCharterFull({ + agentName: 'contract-agent', + charterPath: '.squad/agents/contract-agent/charter.md', + charterContent: CANONICAL + .replace('**Preferred:** auto', '**Preferred:** supported-model') + .replace('**Reasoning Effort:** auto', '**Reasoning Effort:** high') + .replace('**Context Tier:** auto', '**Context Tier:** long_context'), + configOverrides: { + model: 'auto', + reasoningEffort: 'auto', + contextTier: 'auto', + }, + }); + expect(explicitAutoOverride.resolvedModel).toBeUndefined(); + expect(explicitAutoOverride.resolvedReasoningEffort).toBeUndefined(); + expect(explicitAutoOverride.resolvedContextTier).toBeUndefined(); + }); + + it('reports unsupported profiles and extension collisions deterministically', () => { + const unsupported = validateCharterMarkdown(CANONICAL, { + profile: 'squad-charter/v9', + }); + expect(unsupported).toMatchObject({ + accepted: false, + conforms: false, + conformance: 'invalid', + capabilities: [], + }); + expect(unsupported.diagnostics.map(diagnostic => diagnostic.code)).toEqual(['SQC013']); + + const extensions = `${CANONICAL} +## X-Example-Data + +one + +## x-example-data + +two +`; + expect( + validateCharterMarkdown(extensions).diagnostics.map( + diagnostic => diagnostic.code, + ), + ).toEqual(['SQC106', 'SQC012', 'SQC106']); + }); + + it('returns identical diagnostic ordering and ranges across runs', () => { + const markdown = `# Invalid Agent + +## Identity +- **ID:** Invalid Agent +- **Purpose:** +- **Purpose:** duplicate + +## Model +- **Context Tier:** huge +- **Reasoning Effort:** turbo +`; + const first = validateCharterMarkdown(markdown); + const second = validateCharterMarkdown(markdown); + + expect(second).toEqual(first); + expect(first.diagnostics.every(diagnostic => + diagnostic.line >= 1 + && diagnostic.column >= 1 + && diagnostic.endLine >= diagnostic.line + && diagnostic.endColumn >= diagnostic.column, + )).toBe(true); + }); +}); From 91be6a24cb9e2f45e352f53c47f583327d85f7b0 Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Thu, 24 Sep 2026 15:40:45 -0700 Subject: [PATCH 02/11] fix: enforce portable charter conformance Part of #2069 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Source-Commit: ab80da18087b7d37755db2c3cc5880d194a10f52 --- .changeset/formalize-charter-profile.md | 2 +- docs/specification/README.md | 5 +- docs/specification/artifact-inventory.md | 2 + docs/specification/charter-v0.1.md | 146 ++- docs/specification/core-v0.1.md | 60 +- packages/squad-sdk/CHANGELOG.md | 3 +- .../squad-sdk/src/agents/charter-compiler.ts | 119 ++- .../squad-sdk/src/agents/charter-markdown.ts | 31 +- .../squad-sdk/src/agents/charter-validator.ts | 133 ++- packages/squad-sdk/src/agents/index.ts | 16 + packages/squad-sdk/src/parsers.ts | 2 + packages/squad-sdk/src/state/io/charter-io.ts | 30 +- packages/squad-sdk/src/types.ts | 1 + test-fixtures/spec/charter-v0.1/manifest.json | 917 ++++++++++++++++-- test/charter-compiler.test.ts | 20 +- test/charter-spec.test.ts | 576 +++++------ test/config-integration.test.ts | 17 +- 17 files changed, 1559 insertions(+), 521 deletions(-) diff --git a/.changeset/formalize-charter-profile.md b/.changeset/formalize-charter-profile.md index add45dada..5f8e7b8ee 100644 --- a/.changeset/formalize-charter-profile.md +++ b/.changeset/formalize-charter-profile.md @@ -2,4 +2,4 @@ '@bradygaster/squad-sdk': minor --- -Add the draft `squad-charter/v0.1` artifact profile, a shared deterministic Markdown scanner, typed validator diagnostics and capability metadata, canonical and lossless extension-preserving serialization, mandatory conformance fixtures, and implementation-independent specification documents. +Add the draft `squad-charter/v0.1` artifact profile, explicit conformance profile selection, validation-before-runtime compilation, typed diagnostics and capability metadata, canonical and lossless extension-preserving serialization, a language-neutral manifest-driven conformance suite, and implementation-independent specification documents. diff --git a/docs/specification/README.md b/docs/specification/README.md index aeeb93585..8ebbc4605 100644 --- a/docs/specification/README.md +++ b/docs/specification/README.md @@ -2,7 +2,7 @@ **Core version:** 0.1 -**Status:** Draft +**Status:** Working Draft **Document class:** Informative index; linked core/profile documents identify their normative sections @@ -34,5 +34,6 @@ RFC 3339, and CommonMark 0.31.2. The core and each artifact profile are versioned independently. Draft `0.x` documents may change before publication, and unsupported versions fail closed. After publication, a revision is immutable: editorial corrections are recorded -as dated errata, while behavioral changes require a new profile version. See +as dated errata, while normative changes require a new `vMAJOR.MINOR` +identifier. Patch components are not used. See [Core v0.1 §10](core-v0.1.md#10-publication-errata-and-versioning). diff --git a/docs/specification/artifact-inventory.md b/docs/specification/artifact-inventory.md index 8c4047d17..2b04b7555 100644 --- a/docs/specification/artifact-inventory.md +++ b/docs/specification/artifact-inventory.md @@ -1,5 +1,7 @@ # Squad Artifact Inventory +**Document class:** Informative + **Audit date:** 2026-09-24 **Purpose:** Profile planning and parser-drift evidence for issue #2069. diff --git a/docs/specification/charter-v0.1.md b/docs/specification/charter-v0.1.md index a2d8041c1..f4c5e7628 100644 --- a/docs/specification/charter-v0.1.md +++ b/docs/specification/charter-v0.1.md @@ -2,18 +2,38 @@ -**Status:** Draft +**Status:** Working Draft **Document class:** Normative unless a section is marked informative **Profile identifier:** `squad-charter/v0.1` +**Editor:** Squad project maintainers + +**Publication date:** 2026-09-24 + +**Source revision:** `squad-charter-v0.1-wd3` + +**Immutable publication tag:** Not assigned; this is a source-controlled +working draft + +**Feedback:** [Issue #2069](https://github.com/bradygaster/squad/issues/2069) + +**Errata:** [§14](#14-errata) + **Artifact class:** required **Squad-root-relative path:** `agents/{id}/charter.md` **Repository-relative example:** `.squad/agents/{id}/charter.md` +The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, +**SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **NOT RECOMMENDED**, **MAY**, and +**OPTIONAL** in this document are to be interpreted as described in BCP 14 +[RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) +[RFC 8174](https://www.rfc-editor.org/rfc/rfc8174) when, and only when, they +appear in all capitals, as shown here. + ## 1. Purpose and authority A charter identifies one agent and supplies human-readable operating guidance @@ -25,16 +45,31 @@ available, `Identity ID` MUST equal `{id}` after converting Windows separators to `/`. Paths that are neither exactly `agents/{id}/charter.md` nor terminated by `.squad/agents/{id}/charter.md` provide no profile path context. -## 2. Association and unsupported versions +## 2. Normative references + +- [Squad Specification Core v0.1](core-v0.1.md) defines common paths, + conformance, diagnostics, versioning, publication, and trust requirements. +- [BCP 14](https://www.rfc-editor.org/info/bcp14), comprising RFC 2119 and + RFC 8174, defines requirement-keyword interpretation. +- [CommonMark 0.31.2](https://spec.commonmark.org/0.31.2/) defines the Markdown + terms used by the deterministic subset below. + +## 3. Association and unsupported versions The artifact path associates a charter with the charter artifact family. The -profile version is selected explicitly by the calling API or surrounding -manifest; v0.1 has no in-document version marker. +profile version is selected by the calling API or surrounding manifest; v0.1 +has no in-document version marker. + +An API that claims portable conformance MUST require the caller to supply the +profile identifier. Missing selection produces `SQC014`. The TypeScript +reference implementation also exposes `validateCharterMarkdown` as a +convenience API that defaults to `squad-charter/v0.1`; this behavior is not +explicit negotiation, and its result always reports the selected profile. A consumer asked to process a profile other than one it supports MUST return an unsupported-profile error and MUST NOT reinterpret the document as v0.1. -## 3. Canonical document shape +## 4. Canonical document shape ```markdown # Display Name — Role @@ -60,18 +95,21 @@ unsupported-profile error and MUST NOT reinterpret the document as v0.1. The H1, `Identity`, and three canonical responsibility sections define the canonical presentation. `Model` is optional. Producers MUST emit canonical -responsibility headings unless an API caller explicitly requests legacy -output. +sections in this order: `Identity`, `Accountable Responsibilities`, +`Non-Responsibilities`, `Collaboration and Review Authority`, optional +`Model`, then extensions. Producers MUST use the em dash (`—`) H1 separator +and canonical responsibility headings unless an API caller explicitly requests +legacy output. `Purpose` is one non-empty, single-line Markdown field value. The profile does not attempt to determine whether prose is grammatically a sentence. -## 4. Deterministic Markdown subset +## 5. Deterministic Markdown subset Structural recognition uses these rules, in order: -1. Only ATX H1 and H2 headings beginning in column one are structural. Setext - headings and H3-H6 headings are prose. +1. Only ATX H1 and H2 headings beginning in column one are structural. Exactly + one structural H1 is allowed. Setext headings and H3-H6 headings are prose. 2. A machine field is a single line with zero to three leading spaces, an optional list marker, then `**Label:** value`. 3. Headings and fields inside fenced code are ignored. Fences use at least @@ -89,7 +127,7 @@ Structural recognition uses these rules, in order: These rules are the required observable behavior. Implementations need not use the same scanner internally. -## 5. Machine-readable fields +## 6. Machine-readable fields Labels and standard section names are ASCII case-insensitive. Every field in this table has cardinality zero-or-one within its owning section. @@ -98,26 +136,32 @@ this table has cardinality zero-or-one within its owning section. |---|---|---:|---| | Identity | `ID` | required | Lowercase kebab-case: `[a-z0-9]+(?:-[a-z0-9]+)*` | | Identity | `Purpose` | required | Non-empty single-line purpose | -| Identity | `Name` | legacy optional | Display name | -| Identity | `Role` | legacy optional | Role | -| Identity | `Expertise` | legacy optional | Comma-separated informative list | -| Identity | `Style` | legacy optional | Informative style text | +| Identity | `Name` | compatibility only | Display name | +| Identity | `Role` | compatibility only | Role | +| Identity | `Expertise` | compatibility only | Comma-separated informative list | +| Identity | `Style` | compatibility only | Informative style text | | Model | `Preferred` | optional | Model identifier or `auto` | | Model | `Rationale` | optional | Informative prose | -| Model | `Fallback` | legacy optional | Informative hint, not a portable fallback chain | +| Model | `Fallback` | compatibility only | Informative hint, not a portable fallback chain | | Model | `Reasoning Effort` | optional | `auto`, `low`, `medium`, `high`, `xhigh`, or `max` | | Model | `Context Tier` | optional | `auto`, `default`, or `long_context` | Every standard semantic section also has cardinality zero-or-one. A canonical heading and its legacy alias count as the same semantic section. +The compatibility fields `Name`, `Role`, `Expertise`, `Style`, and `Fallback` +are accepted for legacy ingestion but make the document +**noncanonical-compatible**. Editors preserve them when their canonical +semantics cannot be represented without data loss. They are not behavioral +inputs except that `Name` and `Role` may supply legacy display metadata. + Missing behavioral fields mean no charter preference. `auto` preserves the author's request for runtime selection and MUST NOT become a literal runtime override. Unsupported model identifiers MAY be preserved as authored text but MUST NOT be claimed as executable without runtime support. Invalid reasoning or context values are validation errors and MUST NOT become runtime overrides. -## 6. Canonical and legacy handling +## 7. Canonical and legacy handling Consumers MAY accept these legacy aliases: @@ -134,18 +178,24 @@ Consumers MAY accept these legacy aliases: A document with only warning diagnostics is **noncanonical-compatible**, not canonical. Validators expose both canonical `conforms` and broader `accepted` -status. Legacy output requires an explicit editor mode. +status. Legacy output requires an explicit editor mode. Canonical known-field +edits emit standard sections in the order defined by §4. If required canonical +identity semantics are unavailable, the editor preserves compatibility fields +and the result remains noncanonical-compatible rather than inventing values. -## 7. Extensions +## 8. Extensions -Extension headings use `## X--`. Canonical names use lowercase -kebab-case namespace and name segments. Names compare case-insensitively; two -headings that differ only by case collide and are invalid. +Extension headings use `## X--`. The namespace is the first +non-hyphen segment after `X-` and matches `[a-z0-9]+`. The name is every +remaining segment and matches `[a-z0-9]+(?:-[a-z0-9]+)*`. Therefore +`X-example-deploy-policy` has namespace `example` and name `deploy-policy`. +The namespace `squad` is reserved for the Squad specification project. -The namespace owner defines extension semantics. `X-squad-*` is reserved for -the Squad specification project; third parties MUST use a namespace they -control. Unknown consumers MUST treat extension bodies as opaque, untrusted -bytes. +Extension identity is the complete `namespace-name` pair. Comparison is ASCII +case-insensitive; two headings with the same pair collide even when casing +differs. Noncanonical casing produces `SQC106`; a collision additionally +produces `SQC012`. Third parties MUST use a namespace they control. Unknown +consumers MUST treat extension bodies as opaque, untrusted bytes. Editors: @@ -155,7 +205,7 @@ Editors: - MAY rewrite known standard sections into canonical form; - MUST NOT interpret extension body content as standard charter structure. -## 8. Diagnostics +## 9. Diagnostics Missing constructs are anchored at line 1 or, when present, the nearest owning H1/H2 heading. Duplicate diagnostics point to the second and later occurrence. @@ -176,14 +226,18 @@ Diagnostics sort by line, column, then the code order below. | `SQC011` | error | Duplicate standard semantic section | | `SQC012` | error | Case-insensitive extension collision | | `SQC013` | error | Unsupported profile | +| `SQC014` | error | Explicit profile omitted from conformance API | +| `SQC015` | error | Duplicate structural H1 identity | | `SQC101` | warning | Missing canonical Identity section | | `SQC102` | warning | Missing canonical Identity ID | | `SQC103` | warning | Missing canonical Identity Purpose | | `SQC104` | warning | Missing canonical responsibility section | | `SQC105` | warning | Legacy responsibility headings | | `SQC106` | warning | Noncanonical extension heading | +| `SQC107` | warning | Compatibility field present | +| `SQC108` | warning | Unknown or noncanonically ordered standard section | -## 9. Operational capabilities +## 10. Operational capabilities Implementations claim operations individually: @@ -194,18 +248,25 @@ Implementations claim operations individually: | `squad-charter/v0.1/edit` | Canonical serialization plus lossless extension preservation | | `squad-charter/v0.1/legacy-consume` | Accept documented legacy aliases without calling them canonical | -The TypeScript reference implementation exports all four identifiers. A -validation result reports only the validator capability because running -validation does not prove that parsing or editing was exercised. +An implementation claiming a capability MUST pass every manifest case that +lists that capability. A validation result reports only the validator +capability because running validation does not prove that parsing or editing +was exercised. -## 10. Security +## 11. Runtime validation and security Charters are instructions, not authorization. A runtime MUST independently enforce tool, filesystem, network, model, and review policy. It MUST NOT grant capabilities because a charter requests them. Links, commands, encoded text, HTML comments, code blocks, and extension content are untrusted data. -## 11. Mandatory conformance suite +A runtime that applies `Preferred`, `Reasoning Effort`, or `Context Tier` MUST +validate the charter first and MUST reject invalid input without applying any +of those fields. The TypeScript compiler validates before resolving behavior. +Its compatibility path for omitted `charterContent` generates a canonical, +behavior-neutral charter; explicitly empty or invalid content is rejected. + +## 12. Mandatory conformance suite Implementations claiming a capability MUST run the applicable cases in `test-fixtures/spec/charter-v0.1/manifest.json`. The suite defines exact @@ -213,19 +274,24 @@ diagnostic code, severity, location, ordering, path behavior, parsed semantics, capability reporting, profile handling, and round-trip output. It includes every standard field and section duplicate, fenced and opaque fake structure, CRLF behavior, path mismatch, extension ordering, canonical output, legacy -ingestion, and `auto` runtime semantics. +ingestion, and `auto` runtime semantics. The manifest, not the TypeScript +harness, defines case inputs, profile selection, paths, edits, outputs, +classifications, parsed semantics, and ordered diagnostic ranges. -## 12. Informative TypeScript API example +## 13. Informative TypeScript implementation details + +The exported TypeScript names, module paths, and example below are informative. +They do not constrain implementations in other languages. ```ts import { CHARTER_CAPABILITIES, parseCharterMarkdown, serializeCharter, - validateCharterMarkdown, + validateCharterConformance, } from '@bradygaster/squad-sdk/parsers'; -const validation = validateCharterMarkdown(markdown, { +const validation = validateCharterConformance(markdown, { profile: 'squad-charter/v0.1', path: '.squad/agents/reviewer/charter.md', }); @@ -240,3 +306,9 @@ const updated = serializeCharter(charter); console.log(CHARTER_CAPABILITIES.edit, updated); ``` + +## 14. Errata + +No errata are recorded for source revision `squad-charter-v0.1-wd3`. Errata for +a future tagged publication will be listed in this section with publication +date, affected section, and correction. diff --git a/docs/specification/core-v0.1.md b/docs/specification/core-v0.1.md index 52c0e6038..8c19a57de 100644 --- a/docs/specification/core-v0.1.md +++ b/docs/specification/core-v0.1.md @@ -1,11 +1,24 @@ # Squad Specification Core v0.1 -**Status:** Draft +**Status:** Working Draft **Document class:** Normative unless a section is marked informative **Capability identifier:** `squad-core/v0.1` +**Editor:** Squad project maintainers + +**Publication date:** 2026-09-24 + +**Source revision:** `squad-core-v0.1-wd3` + +**Immutable publication tag:** Not assigned; this is a source-controlled +working draft + +**Feedback:** [Issue #2069](https://github.com/bradygaster/squad/issues/2069) + +**Errata:** [§10.1](#101-errata) + ## 1. Scope The core defines rules shared by separately versioned Squad artifact profiles. @@ -21,8 +34,12 @@ telemetry, GitHub workflows, or a whole-Squad conformance claim. terminology used by profiles. A profile may select a deterministic subset rather than require a complete CommonMark implementation. -The keywords **MUST**, **MUST NOT**, **REQUIRED**, **SHOULD**, **SHOULD NOT**, -and **MAY** are normative only when shown in uppercase. +The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, +**SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **NOT RECOMMENDED**, **MAY**, and +**OPTIONAL** in this document are to be interpreted as described in BCP 14 +[RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) +[RFC 8174](https://www.rfc-editor.org/rfc/rfc8174) when, and only when, they +appear in all capitals, as shown here. ## 3. Terms and conformance classes @@ -66,12 +83,18 @@ Every profile MUST assign one of these classes: Classification is profile-specific and MUST NOT be interpreted as a requirement that every Squad installation contain every known artifact. -## 6. Profile association and version handling +## 6. Profile association, selection, and version handling A profile MUST define how an artifact is associated with that profile: path, an in-document marker, an API argument, or an explicit combination. Association MUST NOT rely on prose heuristics. +An API that reports portable conformance MUST require an explicit profile +identifier. Omission is an error, not negotiation. An implementation MAY offer +a separately documented convenience API that defaults to one profile, but that +API MUST identify the selected profile in its result and MUST NOT describe the +default as explicit negotiation. + Consumers MUST fail closed for an unsupported profile version. They MUST NOT silently process an unsupported version as the nearest known version. A consumer MAY expose raw bytes without interpreting them. @@ -121,15 +144,28 @@ for migration or legacy compatibility. ## 10. Publication, errata, and versioning -A published profile revision is immutable. Editorial corrections that do not -change required behavior are recorded as dated errata linked from the -specification index. A behavioral change requires a new profile version. +A core or profile identifier ends in `vMAJOR.MINOR`, where `MAJOR` and `MINOR` +are ASCII decimal integers without leading zeroes except the value `0`. The +grammar is `v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)`. Patch components are not part +of a Squad specification identifier. + +A tagged publication of a core or profile revision is immutable. Editorial +corrections that do not change required behavior are recorded as dated errata +without changing the identifier. A normative change requires a new identifier. +For `v0.x`, every normative change increments `MINOR`. Starting with `v1.0`, +backward-compatible additions increment `MINOR`; changes that can alter +accepted documents or existing semantics increment `MAJOR` and reset `MINOR` +to `0`. + +Working-draft source revisions such as `squad-core-v0.1-wd3` identify review +snapshots but are not immutable publications. Implementations MUST NOT infer +compatibility with an unsupported identifier or source revision. + +### 10.1 Errata -Draft `0.x` documents MAY change before publication. Implementations MUST NOT -infer compatibility with an unrecognized `0.x` version. Stable versions use -semantic intent: patch versions clarify without changing accepted documents, -minor versions add backward-compatible capabilities, and major versions may -change document validity or semantics. +No errata are recorded for source revision `squad-core-v0.1-wd3`. Errata for a +future tagged publication will be listed in this subsection with publication +date, affected section, and correction. ## 11. Trust boundary diff --git a/packages/squad-sdk/CHANGELOG.md b/packages/squad-sdk/CHANGELOG.md index 25863b1b6..3b93da438 100644 --- a/packages/squad-sdk/CHANGELOG.md +++ b/packages/squad-sdk/CHANGELOG.md @@ -9,7 +9,8 @@ - Add the `squad-charter/v0.1` parser, validator, and editor contract. The public parser surface now includes typed diagnostics, profile and capability metadata, canonical/legacy conformance classification, path-aware identity - checks, canonical serialization, and lossless unknown extension preservation. + checks, explicit conformance profile selection, validation-before-compilation, + canonical serialization order, and lossless unknown extension preservation. ## 0.10.0 diff --git a/packages/squad-sdk/src/agents/charter-compiler.ts b/packages/squad-sdk/src/agents/charter-compiler.ts index 6438e65d2..57ab0b8d9 100644 --- a/packages/squad-sdk/src/agents/charter-compiler.ts +++ b/packages/squad-sdk/src/agents/charter-compiler.ts @@ -14,6 +14,11 @@ import { type CharterMarkdownField, type CharterMarkdownScan, } from './charter-markdown.js'; +import { + CHARTER_PROFILE, + validateCharterMarkdown, + type CharterValidationResult, +} from './charter-validator.js'; /** Set form for fast lookup. */ const VALID_EFFORTS = new Set(VALID_REASONING_EFFORTS); @@ -29,8 +34,18 @@ export interface CharterCompileOptions { agentName: string; /** Full path to the agent's charter.md file */ charterPath: string; - /** Raw charter markdown content (if already loaded) */ + /** + * Raw charter markdown content. When omitted, the compatibility compiler + * generates a canonical, behavior-neutral charter from `agentName`. Passing + * an empty or invalid string is rejected. + */ charterContent?: string; + /** + * Charter profile used for validation. The convenience compiler defaults to + * `squad-charter/v0.1`; portable conformance claims must use the explicit + * conformance validator instead. + */ + profile?: string; /** Content of team.md (team roster) */ teamContext?: string; /** Routing rules content */ @@ -114,6 +129,8 @@ export interface CharterSourceMetadata { semanticKey: string; /** Whether the source already uses the canonical responsibility headings */ canonicalShape: boolean; + /** Source line ending used when canonical known-field edits are emitted */ + eol: '\n' | '\r\n' | '\r'; /** Unknown namespaced extension sections in source order, byte-for-byte */ extensions: readonly string[]; } @@ -132,6 +149,8 @@ export interface CompiledCharter extends SquadCustomAgentConfig { resolvedTools?: string[]; /** Parsed charter data */ parsed: ParsedCharter; + /** Validation performed before behavior-affecting fields were applied */ + validation: CharterValidationResult; } /** @@ -156,13 +175,38 @@ export function compileCharterFull(options: CharterCompileOptions): CompiledChar const { agentName, charterPath, charterContent, teamContext, routingRules, decisions, pluginContext, configOverrides } = options; try { - const parsed = parseCharterMarkdown(charterContent ?? ''); + const generatedCompatibility = charterContent === undefined; + const content = charterContent ?? createCompatibilityCharter(agentName); + const validation = validateCharterMarkdown(content, { + path: charterPath, + profile: options.profile ?? CHARTER_PROFILE, + }); + if (!validation.accepted) { + const summary = validation.diagnostics + .filter(diagnostic => diagnostic.severity === 'error') + .map(diagnostic => `${diagnostic.code} at ${diagnostic.line}:${diagnostic.column}`) + .join(', '); + throw new ConfigurationError( + `Invalid charter for agent '${agentName}' at ${charterPath}: ${summary}`, + { + agentName, + operation: 'compileCharter', + timestamp: new Date(), + metadata: { + charterPath, + profile: validation.profile, + diagnostics: validation.diagnostics, + }, + }, + ); + } + const parsed = parseCharterMarkdown(content); // Build the complete prompt by composing sections const promptParts: string[] = []; // Add charter content - promptParts.push(parsed.fullContent || `# ${agentName} Charter\n\nAgent charter content.`); + promptParts.push(parsed.fullContent); // Add team context if available if (teamContext) { @@ -195,7 +239,11 @@ export function compileCharterFull(options: CharterCompileOptions): CompiledChar // Config overrides win over charter-parsed values const role = configOverrides?.role || parsed.identity.role; const displayName = configOverrides?.displayName - || (role ? `${capitalize(agentName)} — ${role}` : capitalize(agentName)); + || (generatedCompatibility + ? capitalize(agentName) + : role + ? `${capitalize(agentName)} — ${role}` + : capitalize(agentName)); const description = parsed.identity.expertise?.length ? `Expertise: ${parsed.identity.expertise.join(', ')}` @@ -242,6 +290,7 @@ export function compileCharterFull(options: CharterCompileOptions): CompiledChar resolvedContextTier, resolvedTools, parsed, + validation, }; } catch (error) { if (error instanceof ConfigurationError) throw error; @@ -371,6 +420,7 @@ export function parseCharterMarkdown(content: string): ParsedCharter { value: { semanticKey: charterSemanticKey(result), canonicalShape: hasCanonicalShape(scan), + eol: scan.eol, extensions: scan.sections .filter(section => section.isExtension) .map(section => section.raw), @@ -431,14 +481,40 @@ function stripCode(value: string): string { } function hasCanonicalShape(scan: CharterMarkdownScan): boolean { - const names = new Set(scan.sections.map(section => section.name.toLowerCase())); - return names.has('identity') - && names.has('accountable responsibilities') - && names.has('non-responsibilities') - && names.has('collaboration and review authority') - && !names.has('what i own') - && !names.has('boundaries') - && !names.has('collaboration'); + if (scan.h1s.length !== 1 || !/^.+?\s+—\s+.+?$/.test(scan.h1s[0]!.name)) { + return false; + } + + const standardSections = scan.sections.filter(section => !section.isExtension); + const names = standardSections.map(section => section.name.toLowerCase()); + const expected = [ + 'identity', + 'accountable responsibilities', + 'non-responsibilities', + 'collaboration and review authority', + ]; + if (names.length === expected.length + 1) expected.push('model'); + if (names.length !== expected.length || names.some((name, index) => name !== expected[index])) { + return false; + } + + const canonicalExtensions = scan.sections + .filter(section => section.isExtension) + .every(section => /^X-[a-z0-9]+-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(section.name)); + if (!canonicalExtensions) return false; + + const compatibilityFields = new Set(['name', 'role', 'expertise', 'style', 'fallback']); + if (scan.fields.some(field => compatibilityFields.has(field.name.toLowerCase()))) { + return false; + } + + const identityFields = fieldsInSection(scan, 'Identity'); + const ids = identityFields.filter(field => field.name.toLowerCase() === 'id'); + const purposes = identityFields.filter(field => field.name.toLowerCase() === 'purpose'); + return ids.length === 1 + && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(stripCode(ids[0]!.value)) + && purposes.length === 1 + && purposes[0]!.value.trim().length > 0; } /** @@ -448,3 +524,22 @@ function capitalize(str: string): string { if (!str) return str; return str.charAt(0).toUpperCase() + str.slice(1); } + +function createCompatibilityCharter(agentName: string): string { + const displayName = capitalize(agentName); + return `# ${displayName} — Agent + +## Identity +- **ID:** \`${agentName}\` +- **Purpose:** Provide compatibility behavior when no charter content was supplied. + +## Accountable Responsibilities +- Follow the caller-provided task. + +## Non-Responsibilities +- No additional responsibilities are inferred. + +## Collaboration and Review Authority +- Follow caller-provided collaboration and review instructions. +`; +} diff --git a/packages/squad-sdk/src/agents/charter-markdown.ts b/packages/squad-sdk/src/agents/charter-markdown.ts index cbde4cab9..7f53bb13c 100644 --- a/packages/squad-sdk/src/agents/charter-markdown.ts +++ b/packages/squad-sdk/src/agents/charter-markdown.ts @@ -11,6 +11,7 @@ export interface CharterMarkdownHeading { readonly name: string; readonly line: number; readonly column: number; + readonly width: number; readonly start: number; readonly isExtension: boolean; } @@ -20,6 +21,7 @@ export interface CharterMarkdownField { readonly value: string; readonly line: number; readonly column: number; + readonly width: number; readonly section: string; } @@ -33,6 +35,7 @@ export interface CharterMarkdownScan { readonly markdown: string; readonly eol: '\n' | '\r\n' | '\r'; readonly h1?: CharterMarkdownHeading; + readonly h1s: readonly CharterMarkdownHeading[]; readonly sections: readonly CharterMarkdownSection[]; readonly fields: readonly CharterMarkdownField[]; } @@ -64,6 +67,7 @@ export function scanCharterMarkdown(markdown: string): CharterMarkdownScan { const lines = splitSourceLines(markdown); const sections: CharterMarkdownSection[] = []; const fields: CharterMarkdownField[] = []; + const h1s: CharterMarkdownHeading[] = []; let h1: CharterMarkdownHeading | undefined; let currentSection: PendingSection | undefined; let extensionTail = false; @@ -136,6 +140,7 @@ export function scanCharterMarkdown(markdown: string): CharterMarkdownScan { name, line: sourceLine.line, column: text.indexOf('##') + 1, + width: text.length, start: sourceLine.start, headingEnd: sourceLine.end - sourceLine.eol.length, isExtension, @@ -147,17 +152,19 @@ export function scanCharterMarkdown(markdown: string): CharterMarkdownScan { if (extensionTail) continue; - if (!h1) { - const h1Match = text.match(H1); - if (h1Match) { - h1 = { - name: h1Match[1]!.trim(), - line: sourceLine.line, - column: text.indexOf('#') + 1, - start: sourceLine.start, - isExtension: false, - }; - } + const h1Match = text.match(H1); + if (h1Match) { + const heading: CharterMarkdownHeading = { + name: h1Match[1]!.trim(), + line: sourceLine.line, + column: text.indexOf('#') + 1, + width: text.length, + start: sourceLine.start, + isExtension: false, + }; + h1s.push(heading); + h1 ??= heading; + continue; } if (!currentSection) continue; @@ -168,6 +175,7 @@ export function scanCharterMarkdown(markdown: string): CharterMarkdownScan { value: fieldMatch[2]!, line: sourceLine.line, column: text.indexOf('**') + 1, + width: fieldMatch[1]!.trim().length + 5, section: currentSection.name, }); } @@ -179,6 +187,7 @@ export function scanCharterMarkdown(markdown: string): CharterMarkdownScan { markdown, eol: detectEol(markdown), ...(h1 ? { h1 } : {}), + h1s, sections, fields, }; diff --git a/packages/squad-sdk/src/agents/charter-validator.ts b/packages/squad-sdk/src/agents/charter-validator.ts index 8f11b92ff..0ecbfd876 100644 --- a/packages/squad-sdk/src/agents/charter-validator.ts +++ b/packages/squad-sdk/src/agents/charter-validator.ts @@ -38,12 +38,16 @@ export const CHARTER_DIAGNOSTIC_CODES = [ 'SQC011', 'SQC012', 'SQC013', + 'SQC014', + 'SQC015', 'SQC101', 'SQC102', 'SQC103', 'SQC104', 'SQC105', 'SQC106', + 'SQC107', + 'SQC108', ] as const; export type CharterDiagnosticCode = typeof CHARTER_DIAGNOSTIC_CODES[number]; @@ -77,6 +81,11 @@ export interface CharterValidationOptions { profile?: string; } +export interface CharterConformanceOptions extends CharterValidationOptions { + /** Explicit profile selection is required for a conformance claim. */ + profile: string; +} + export interface CharterValidationResult { profile: string; /** True only for canonical profile documents. */ @@ -94,7 +103,7 @@ interface FieldDefinition { } const ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; -const EXTENSION_PATTERN = /^X-[a-z0-9]+(?:-[a-z0-9]+)*-[a-z0-9]+(?:-[a-z0-9]+)*$/; +const EXTENSION_PATTERN = /^X-([a-z0-9]+)-([a-z0-9]+(?:-[a-z0-9]+)*)$/; const CANONICAL_RESPONSIBILITY_SECTIONS = [ 'Accountable Responsibilities', 'Non-Responsibilities', @@ -130,6 +139,13 @@ const UNIQUE_MACHINE_FIELDS: readonly FieldDefinition[] = [ { name: 'Reasoning Effort', section: 'Model' }, { name: 'Context Tier', section: 'Model' }, ] as const; +const COMPATIBILITY_FIELDS: readonly FieldDefinition[] = [ + { name: 'Name', section: 'Identity' }, + { name: 'Role', section: 'Identity' }, + { name: 'Expertise', section: 'Identity' }, + { name: 'Style', section: 'Identity' }, + { name: 'Fallback', section: 'Model' }, +] as const; const VALID_EFFORTS = new Set(VALID_REASONING_EFFORTS); const VALID_TIERS = new Set(VALID_CONTEXT_TIERS); const DIAGNOSTIC_ORDER = new Map( @@ -146,7 +162,28 @@ export function validateCharterMarkdown( markdown: string, options: CharterValidationOptions = {}, ): CharterValidationResult { - const requestedProfile = options.profile ?? CHARTER_PROFILE; + return validateCharter(markdown, options, false); +} + +/** + * Validate a charter for an explicit portable conformance claim. + * + * Unlike {@link validateCharterMarkdown}, this API never assumes a profile. + * JavaScript callers that omit `profile` receive SQC014. + */ +export function validateCharterConformance( + markdown: string, + options: CharterConformanceOptions, +): CharterValidationResult { + return validateCharter(markdown, options, true); +} + +function validateCharter( + markdown: string, + options: CharterValidationOptions | undefined, + requireExplicitProfile: boolean, +): CharterValidationResult { + const requestedProfile = options?.profile; const diagnostics: CharterDiagnostic[] = []; const add = ( code: CharterDiagnosticCode, @@ -164,29 +201,40 @@ export function validateCharterMarkdown( column, endLine: location.line, endColumn: column + width - 1, - ...(options.path ? { path: options.path } : {}), + ...(options?.path ? { path: options.path } : {}), }); }; - if (requestedProfile !== CHARTER_PROFILE) { + if (requireExplicitProfile && requestedProfile === undefined) { + add( + 'SQC014', + 'error', + 'portable conformance validation requires an explicit profile', + { line: 1 }, + ); + return createResult('', diagnostics, []); + } + + const selectedProfile = requestedProfile ?? CHARTER_PROFILE; + if (selectedProfile !== CHARTER_PROFILE) { add( 'SQC013', 'error', - `unsupported charter profile "${requestedProfile}"`, + `unsupported charter profile "${selectedProfile}"`, { line: 1 }, ); - return createResult(requestedProfile, diagnostics, []); + return createResult(selectedProfile, diagnostics, []); } const scan = scanCharterMarkdown(markdown); if (!markdown.trim()) { add('SQC001', 'error', 'charter.md must not be empty', { line: 1 }); - return createResult(requestedProfile, diagnostics); + return createResult(selectedProfile, diagnostics); } if (!scan.h1) { add('SQC002', 'error', 'charter.md must contain an H1 identity heading', { line: 1 }); - } else if (!/^.+?\s+(?:—|–|-)\s+.+?$/.test(scan.h1.name)) { + } else if (!/^.+?\s+—\s+.+?$/.test(scan.h1.name)) { add( 'SQC003', 'warning', @@ -195,6 +243,15 @@ export function validateCharterMarkdown( ); } + for (const duplicate of scan.h1s.slice(1)) { + add( + 'SQC015', + 'error', + 'charter.md must contain at most one structural H1 identity', + headingLocation(duplicate), + ); + } + const identityHeadings = headingsNamed(scan.sections, 'Identity'); const identityHeading = identityHeadings[0]; if (!identityHeading) { @@ -245,7 +302,7 @@ export function validateCharterMarkdown( ); } - const pathId = profileIdentityFromPath(options.path); + const pathId = profileIdentityFromPath(options?.path); if (pathId !== undefined && pathId !== normalizedId) { add( 'SQC010', @@ -306,6 +363,43 @@ export function validateCharterMarkdown( ); } + const canonicalSectionNames = [ + 'Identity', + ...CANONICAL_RESPONSIBILITY_SECTIONS, + 'Model', + ]; + const standardSections = scan.sections.filter(section => !section.isExtension); + const unknownSection = standardSections.find(section => + !canonicalSectionNames.some(name => equalName(name, section.name)) + && !LEGACY_RESPONSIBILITY_SECTIONS.some(name => equalName(name, section.name)), + ); + if (unknownSection) { + add( + 'SQC108', + 'warning', + `section "${unknownSection.name}" is not part of the canonical v0.1 shape`, + headingLocation(unknownSection), + ); + } + + const canonicalSections = standardSections.filter(section => + canonicalSectionNames.some(name => equalName(name, section.name)), + ); + const canonicalOrder = canonicalSections.map(section => + canonicalSectionNames.findIndex(name => equalName(name, section.name)), + ); + const outOfOrderIndex = canonicalOrder.findIndex( + (order, index) => index > 0 && order < canonicalOrder[index - 1]!, + ); + if (outOfOrderIndex >= 0) { + add( + 'SQC108', + 'warning', + 'standard sections are not in canonical v0.1 order', + headingLocation(canonicalSections[outOfOrderIndex]!), + ); + } + const preferred = findField(scan.fields, 'Preferred', 'Model'); if (preferred && !preferred.value.trim()) { add( @@ -340,6 +434,21 @@ export function validateCharterMarkdown( fieldLocation(contextTier), ); } + + for (const compatibilityField of COMPATIBILITY_FIELDS) { + for (const field of findFields( + scan.fields, + compatibilityField.name, + compatibilityField.section, + )) { + add( + 'SQC107', + 'warning', + `compatibility field "${compatibilityField.name}" is accepted but is not canonical v0.1`, + fieldLocation(field), + ); + } + } } const extensions = scan.sections.filter(section => section.isExtension); @@ -372,7 +481,7 @@ export function validateCharterMarkdown( || diagnosticIndex(left.code) - diagnosticIndex(right.code), ); - return createResult(requestedProfile, diagnostics); + return createResult(selectedProfile, diagnostics); } function createResult( @@ -450,7 +559,7 @@ function headingLocation( return { line: heading.line, column: heading.column, - width: heading.name.length + 3, + width: heading.width, }; } @@ -460,7 +569,7 @@ function fieldLocation( return { line: field.line, column: field.column, - width: field.name.length + 5, + width: field.width, }; } diff --git a/packages/squad-sdk/src/agents/index.ts b/packages/squad-sdk/src/agents/index.ts index 0d2141400..6920b3b84 100644 --- a/packages/squad-sdk/src/agents/index.ts +++ b/packages/squad-sdk/src/agents/index.ts @@ -12,6 +12,7 @@ import type { StorageProvider } from '../storage/storage-provider.js'; import type { SquadState } from '../state/squad-state.js'; import { randomUUID } from 'node:crypto'; import { parseCharterMarkdown } from './charter-compiler.js'; +import { validateCharterMarkdown } from './charter-validator.js'; import { EventBus } from '../client/event-bus.js'; import { trace, SpanStatusCode } from '../runtime/otel-api.js'; import { recordAgentSpawn, recordAgentDuration, recordAgentError, recordAgentDestroy } from '../runtime/otel-metrics.js'; @@ -48,9 +49,11 @@ export { CHARTER_DIAGNOSTIC_SEVERITIES, CHARTER_PROFILE, CHARTER_PROFILE_METADATA, + validateCharterConformance, validateCharterMarkdown, type CharterCapability, type CharterConformance, + type CharterConformanceOptions, type CharterDiagnostic, type CharterDiagnosticCode, type CharterDiagnosticSeverity, @@ -184,6 +187,7 @@ export class CharterCompiler { if (content === undefined) { throw new Error(`Charter file not found: ${charterPath}`); } + assertValidRuntimeCharter(content, charterPath); const parsed = parseCharterMarkdown(content); const name = parsed.identity.name ?? basename(dirname(charterPath)); @@ -214,6 +218,7 @@ export class CharterCompiler { throw new Error('compileByName requires SquadState — pass state to CharterCompiler constructor'); } const content = await this.state.agents.get(agentName).charter(); + assertValidRuntimeCharter(content, `agents/${agentName}/charter.md`); const parsed = parseCharterMarkdown(content); const name = parsed.identity.name ?? agentName; @@ -284,6 +289,17 @@ export class CharterCompiler { } } +function assertValidRuntimeCharter(content: string, charterPath: string): void { + const validation = validateCharterMarkdown(content, { path: charterPath }); + if (validation.accepted) return; + + const summary = validation.diagnostics + .filter(diagnostic => diagnostic.severity === 'error') + .map(diagnostic => `${diagnostic.code} at ${diagnostic.line}:${diagnostic.column}`) + .join(', '); + throw new Error(`Invalid charter at ${charterPath}: ${summary}`); +} + // --- Agent Session Manager --- export class AgentSessionManager { diff --git a/packages/squad-sdk/src/parsers.ts b/packages/squad-sdk/src/parsers.ts index 826afa112..ce12dc3b5 100644 --- a/packages/squad-sdk/src/parsers.ts +++ b/packages/squad-sdk/src/parsers.ts @@ -51,12 +51,14 @@ export { CHARTER_DIAGNOSTIC_SEVERITIES, CHARTER_PROFILE, CHARTER_PROFILE_METADATA, + validateCharterConformance, validateCharterMarkdown, } from './agents/charter-validator.js'; export type { CharterCapability, CharterConformance, + CharterConformanceOptions, CharterDiagnostic, CharterDiagnosticCode, CharterDiagnosticSeverity, diff --git a/packages/squad-sdk/src/state/io/charter-io.ts b/packages/squad-sdk/src/state/io/charter-io.ts index 5d142d56d..4ef6a3d29 100644 --- a/packages/squad-sdk/src/state/io/charter-io.ts +++ b/packages/squad-sdk/src/state/io/charter-io.ts @@ -61,6 +61,7 @@ export function serializeCharter( } const lines: string[] = []; + const eol = charter.source?.eol ?? '\n'; // Title line const name = charter.identity.name ?? 'Agent'; @@ -68,8 +69,8 @@ export function serializeCharter( lines.push(`# ${name} — ${role}`); lines.push(''); - // Style quote - if (charter.identity.style) { + // Compatibility-only style prose is emitted only in explicit legacy mode. + if (format === 'legacy' && charter.identity.style) { lines.push(`> ${charter.identity.style}`); lines.push(''); } @@ -89,7 +90,10 @@ export function serializeCharter( if (!charter.identity.purpose && charter.identity.role) { lines.push(`- **Role:** ${charter.identity.role}`); } - if (charter.identity.expertise && charter.identity.expertise.length > 0) { + if ( + charter.identity.expertise + && charter.identity.expertise.length > 0 + ) { lines.push(`- **Expertise:** ${charter.identity.expertise.join(', ')}`); } if (charter.identity.style) { @@ -125,6 +129,14 @@ export function serializeCharter( lines.push(''); } + // Collaboration section + if (charter.collaboration !== undefined) { + lines.push(`## ${headings.collaboration}`); + lines.push(''); + lines.push(charter.collaboration); + lines.push(''); + } + // Model section if ( charter.modelPreference @@ -157,17 +169,9 @@ export function serializeCharter( lines.push(''); } - // Collaboration section - if (charter.collaboration !== undefined) { - lines.push(`## ${headings.collaboration}`); - lines.push(''); - lines.push(charter.collaboration); - lines.push(''); - } - - const canonical = lines.join('\n').trimEnd() + '\n'; + const canonical = lines.join(eol).trimEnd() + eol; const extensions = charter.source?.extensions.join('') ?? ''; return extensions - ? `${canonical.trimEnd()}\n\n${extensions}` + ? `${canonical.trimEnd()}${eol}${eol}${extensions}` : canonical; } diff --git a/packages/squad-sdk/src/types.ts b/packages/squad-sdk/src/types.ts index e5fe0dda8..03316d608 100644 --- a/packages/squad-sdk/src/types.ts +++ b/packages/squad-sdk/src/types.ts @@ -21,6 +21,7 @@ export type { CharterSourceMetadata } from './agents/charter-compiler.js'; export type { CompiledCharter } from './agents/charter-compiler.js'; export type { CharterCapability } from './agents/charter-validator.js'; export type { CharterConformance } from './agents/charter-validator.js'; +export type { CharterConformanceOptions } from './agents/charter-validator.js'; export type { CharterDiagnostic } from './agents/charter-validator.js'; export type { CharterDiagnosticCode } from './agents/charter-validator.js'; export type { CharterDiagnosticSeverity } from './agents/charter-validator.js'; diff --git a/test-fixtures/spec/charter-v0.1/manifest.json b/test-fixtures/spec/charter-v0.1/manifest.json index c85359dc1..39a3aec61 100644 --- a/test-fixtures/spec/charter-v0.1/manifest.json +++ b/test-fixtures/spec/charter-v0.1/manifest.json @@ -1,87 +1,882 @@ { + "schemaVersion": 1, "profile": "squad-charter/v0.1", - "requiredCapabilities": [ + "capabilities": [ "squad-charter/v0.1/parse", "squad-charter/v0.1/validate", "squad-charter/v0.1/edit", "squad-charter/v0.1/legacy-consume" ], - "fixtures": [ + "cases": [ { - "path": "positive/canonical.md", + "id": "canonical-document", + "capabilities": [ + "squad-charter/v0.1/parse", + "squad-charter/v0.1/validate" + ], + "input": { + "fixture": "positive/canonical.md", + "text": null, + "path": "agents/architect/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": null, "expected": { - "accepted": true, - "conforms": true, - "conformance": "canonical", - "diagnostics": [], - "identityId": "architect", - "reasoningEffort": "high", - "contextTier": "long_context" + "validation": { + "profile": "squad-charter/v0.1", + "accepted": true, + "conforms": true, + "classification": "canonical", + "capabilities": ["squad-charter/v0.1/validate"], + "diagnostics": [] + }, + "parsed": { + "identity": { + "id": "architect", + "purpose": "Keep cross-package architecture coherent.", + "name": "Architect", + "role": "Lead Architect", + "expertise": null, + "style": null + }, + "ownership": "- Cross-package architecture and compatibility strategy.", + "boundaries": "- Routine implementation owned by domain specialists.", + "collaboration": "- Adjudicates compatibility disputes.", + "modelPreference": "auto", + "modelRationale": null, + "modelFallback": null, + "reasoningEffort": "high", + "authoredReasoningEffort": "high", + "contextTier": "long_context", + "authoredContextTier": "long_context" + }, + "serialization": null, + "runtime": null } }, { - "path": "negative/invalid-behavior.md", + "id": "canonical-known-field-edit-orders-collaboration-before-model", + "capabilities": [ + "squad-charter/v0.1/parse", + "squad-charter/v0.1/validate", + "squad-charter/v0.1/edit" + ], + "input": { + "fixture": "positive/canonical.md", + "text": null, + "path": "agents/architect/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": { + "mode": "canonical", + "edits": [ + { + "field": "identity.purpose", + "value": "Keep architecture and conformance coherent." + } + ] + }, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": true, + "conforms": true, + "classification": "canonical", + "capabilities": ["squad-charter/v0.1/validate"], + "diagnostics": [] + }, + "parsed": { + "identity": { + "id": "architect", + "purpose": "Keep cross-package architecture coherent.", + "name": "Architect", + "role": "Lead Architect", + "expertise": null, + "style": null + }, + "ownership": "- Cross-package architecture and compatibility strategy.", + "boundaries": "- Routine implementation owned by domain specialists.", + "collaboration": "- Adjudicates compatibility disputes.", + "modelPreference": "auto", + "modelRationale": null, + "modelFallback": null, + "reasoningEffort": "high", + "authoredReasoningEffort": "high", + "contextTier": "long_context", + "authoredContextTier": "long_context" + }, + "serialization": { + "output": "# Architect — Lead Architect\n\n## Identity\n\n- **ID:** `architect`\n- **Purpose:** Keep architecture and conformance coherent.\n\n## Accountable Responsibilities\n\n- Cross-package architecture and compatibility strategy.\n\n## Non-Responsibilities\n\n- Routine implementation owned by domain specialists.\n\n## Collaboration and Review Authority\n\n- Adjudicates compatibility disputes.\n\n## Model\n\n**Preferred:** auto\n**Reasoning Effort:** high\n**Context Tier:** long_context\n", + "outputFixture": null, + "eol": "LF", + "validation": { + "accepted": true, + "conforms": true, + "classification": "canonical", + "diagnostics": [] + } + }, + "runtime": null + } + }, + { + "id": "invalid-behavior-is-rejected-before-runtime-application", + "capabilities": ["squad-charter/v0.1/validate"], + "input": { + "fixture": "negative/invalid-behavior.md", + "text": null, + "path": "docs/invalid-behavior.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": null, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": false, + "conforms": false, + "classification": "invalid", + "capabilities": ["squad-charter/v0.1/validate"], + "diagnostics": [ + { + "code": "SQC005", + "severity": "error", + "start": {"line": 4, "column": 3}, + "end": {"line": 4, "column": 9}, + "path": "docs/invalid-behavior.md" + }, + { + "code": "SQC007", + "severity": "error", + "start": {"line": 17, "column": 3}, + "end": {"line": 17, "column": 23}, + "path": "docs/invalid-behavior.md" + }, + { + "code": "SQC004", + "severity": "error", + "start": {"line": 18, "column": 3}, + "end": {"line": 18, "column": 23}, + "path": "docs/invalid-behavior.md" + }, + { + "code": "SQC008", + "severity": "error", + "start": {"line": 19, "column": 3}, + "end": {"line": 19, "column": 19}, + "path": "docs/invalid-behavior.md" + } + ] + }, + "parsed": null, + "serialization": null, + "runtime": { + "compile": false, + "errorCodes": ["SQC005", "SQC007", "SQC004", "SQC008"], + "resolvedModel": null, + "resolvedReasoningEffort": null, + "resolvedContextTier": null + } + } + }, + { + "id": "legacy-ingestion-and-canonical-migration", + "capabilities": [ + "squad-charter/v0.1/parse", + "squad-charter/v0.1/validate", + "squad-charter/v0.1/edit", + "squad-charter/v0.1/legacy-consume" + ], + "input": { + "fixture": "legacy/template-style.md", + "text": null, + "path": "agents/legacy-agent/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": { + "mode": "canonical", + "edits": [] + }, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": true, + "conforms": false, + "classification": "noncanonical-compatible", + "capabilities": ["squad-charter/v0.1/validate"], + "diagnostics": [ + {"code": "SQC104", "severity": "warning", "start": {"line": 1, "column": 1}, "end": {"line": 1, "column": 1}, "path": "agents/legacy-agent/charter.md"}, + {"code": "SQC104", "severity": "warning", "start": {"line": 1, "column": 1}, "end": {"line": 1, "column": 1}, "path": "agents/legacy-agent/charter.md"}, + {"code": "SQC104", "severity": "warning", "start": {"line": 1, "column": 1}, "end": {"line": 1, "column": 1}, "path": "agents/legacy-agent/charter.md"}, + {"code": "SQC102", "severity": "warning", "start": {"line": 3, "column": 1}, "end": {"line": 3, "column": 1}, "path": "agents/legacy-agent/charter.md"}, + {"code": "SQC103", "severity": "warning", "start": {"line": 3, "column": 1}, "end": {"line": 3, "column": 1}, "path": "agents/legacy-agent/charter.md"}, + {"code": "SQC107", "severity": "warning", "start": {"line": 4, "column": 3}, "end": {"line": 4, "column": 11}, "path": "agents/legacy-agent/charter.md"}, + {"code": "SQC107", "severity": "warning", "start": {"line": 5, "column": 3}, "end": {"line": 5, "column": 11}, "path": "agents/legacy-agent/charter.md"}, + {"code": "SQC107", "severity": "warning", "start": {"line": 6, "column": 3}, "end": {"line": 6, "column": 16}, "path": "agents/legacy-agent/charter.md"}, + {"code": "SQC107", "severity": "warning", "start": {"line": 7, "column": 3}, "end": {"line": 7, "column": 12}, "path": "agents/legacy-agent/charter.md"}, + {"code": "SQC105", "severity": "warning", "start": {"line": 9, "column": 1}, "end": {"line": 9, "column": 13}, "path": "agents/legacy-agent/charter.md"} + ] + }, + "parsed": { + "identity": { + "id": null, + "purpose": null, + "name": "Legacy Agent", + "role": "Developer", + "expertise": ["TypeScript", "testing"], + "style": "Direct" + }, + "ownership": "- Legacy compatibility.", + "boundaries": "- Does not own architecture.", + "collaboration": "- Works with the lead.", + "modelPreference": "auto", + "modelRationale": null, + "modelFallback": null, + "reasoningEffort": null, + "authoredReasoningEffort": "auto", + "contextTier": null, + "authoredContextTier": "auto" + }, + "serialization": { + "output": "# Legacy Agent — Developer\n\n## Identity\n\n- **Name:** Legacy Agent\n- **Role:** Developer\n- **Expertise:** TypeScript, testing\n- **Style:** Direct\n\n## Accountable Responsibilities\n\n- Legacy compatibility.\n\n## Non-Responsibilities\n\n- Does not own architecture.\n\n## Collaboration and Review Authority\n\n- Works with the lead.\n\n## Model\n\n**Preferred:** auto\n**Reasoning Effort:** auto\n**Context Tier:** auto\n", + "outputFixture": null, + "eol": "LF", + "validation": { + "accepted": true, + "conforms": false, + "classification": "noncanonical-compatible", + "diagnostics": [ + {"code": "SQC102", "severity": "warning", "start": {"line": 3, "column": 1}, "end": {"line": 3, "column": 1}, "path": "agents/legacy-agent/charter.md"}, + {"code": "SQC103", "severity": "warning", "start": {"line": 3, "column": 1}, "end": {"line": 3, "column": 1}, "path": "agents/legacy-agent/charter.md"}, + {"code": "SQC107", "severity": "warning", "start": {"line": 5, "column": 3}, "end": {"line": 5, "column": 11}, "path": "agents/legacy-agent/charter.md"}, + {"code": "SQC107", "severity": "warning", "start": {"line": 6, "column": 3}, "end": {"line": 6, "column": 11}, "path": "agents/legacy-agent/charter.md"}, + {"code": "SQC107", "severity": "warning", "start": {"line": 7, "column": 3}, "end": {"line": 7, "column": 16}, "path": "agents/legacy-agent/charter.md"}, + {"code": "SQC107", "severity": "warning", "start": {"line": 8, "column": 3}, "end": {"line": 8, "column": 12}, "path": "agents/legacy-agent/charter.md"} + ] + } + }, + "runtime": null + } + }, + { + "id": "extension-no-op-is-byte-exact", + "capabilities": [ + "squad-charter/v0.1/parse", + "squad-charter/v0.1/validate", + "squad-charter/v0.1/edit" + ], + "input": { + "fixture": "round-trip/extensions.md", + "text": null, + "path": "agents/extensible-agent/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": { + "mode": "canonical", + "edits": [] + }, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": true, + "conforms": true, + "classification": "canonical", + "capabilities": ["squad-charter/v0.1/validate"], + "diagnostics": [] + }, + "parsed": { + "identity": { + "id": "extensible-agent", + "purpose": "Verify unknown extension preservation requirements.", + "name": "Extensible Agent", + "role": "Integrator", + "expertise": null, + "style": null + }, + "ownership": "- Integration behavior.", + "boundaries": "- Extension interpretation.", + "collaboration": "- Coordinates with extension owners.", + "modelPreference": null, + "modelRationale": null, + "modelFallback": null, + "reasoningEffort": null, + "authoredReasoningEffort": null, + "contextTier": null, + "authoredContextTier": null + }, + "serialization": { + "output": null, + "outputFixture": "round-trip/extensions.md", + "eol": "LF", + "validation": { + "accepted": true, + "conforms": true, + "classification": "canonical", + "diagnostics": [] + } + }, + "runtime": null + } + }, + { + "id": "extension-edit-preserves-bytes-and-order", + "capabilities": [ + "squad-charter/v0.1/parse", + "squad-charter/v0.1/validate", + "squad-charter/v0.1/edit" + ], + "input": { + "fixture": "round-trip/extensions.md", + "text": null, + "path": "agents/extensible-agent/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": { + "mode": "canonical", + "edits": [ + { + "field": "identity.purpose", + "value": "Verify edited fields without losing unknown extensions." + } + ] + }, "expected": { - "accepted": false, - "conforms": false, - "conformance": "invalid", - "diagnostics": [ - { "code": "SQC005", "severity": "error", "line": 4, "column": 3 }, - { "code": "SQC007", "severity": "error", "line": 17, "column": 3 }, - { "code": "SQC004", "severity": "error", "line": 18, "column": 3 }, - { "code": "SQC008", "severity": "error", "line": 19, "column": 3 } + "validation": { + "profile": "squad-charter/v0.1", + "accepted": true, + "conforms": true, + "classification": "canonical", + "capabilities": ["squad-charter/v0.1/validate"], + "diagnostics": [] + }, + "parsed": { + "identity": { + "id": "extensible-agent", + "purpose": "Verify unknown extension preservation requirements.", + "name": "Extensible Agent", + "role": "Integrator", + "expertise": null, + "style": null + }, + "ownership": "- Integration behavior.", + "boundaries": "- Extension interpretation.", + "collaboration": "- Coordinates with extension owners.", + "modelPreference": null, + "modelRationale": null, + "modelFallback": null, + "reasoningEffort": null, + "authoredReasoningEffort": null, + "contextTier": null, + "authoredContextTier": null + }, + "serialization": { + "output": null, + "outputFixture": "round-trip/extensions.edited.md", + "eol": "LF", + "validation": { + "accepted": true, + "conforms": true, + "classification": "canonical", + "diagnostics": [] + } + }, + "runtime": null + } + }, + { + "id": "crlf-known-edit-preserves-line-endings", + "capabilities": [ + "squad-charter/v0.1/parse", + "squad-charter/v0.1/validate", + "squad-charter/v0.1/edit" + ], + "input": { + "fixture": null, + "text": "# CRLF Agent — Tester\r\n\r\n## Identity\r\n- **ID:** `crlf-agent`\r\n- **Purpose:** Original purpose.\r\n\r\n## Accountable Responsibilities\r\n- Test CRLF.\r\n\r\n## Non-Responsibilities\r\n- LF normalization.\r\n\r\n## Collaboration and Review Authority\r\n- Review byte preservation.\r\n\r\n## X-example-data\r\n\r\nopaque: true\r\n", + "path": "agents/crlf-agent/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": { + "mode": "canonical", + "edits": [ + { + "field": "identity.purpose", + "value": "Edited purpose." + } ] + }, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": true, + "conforms": true, + "classification": "canonical", + "capabilities": ["squad-charter/v0.1/validate"], + "diagnostics": [] + }, + "parsed": { + "identity": { + "id": "crlf-agent", + "purpose": "Original purpose.", + "name": "CRLF Agent", + "role": "Tester", + "expertise": null, + "style": null + }, + "ownership": "- Test CRLF.", + "boundaries": "- LF normalization.", + "collaboration": "- Review byte preservation.", + "modelPreference": null, + "modelRationale": null, + "modelFallback": null, + "reasoningEffort": null, + "authoredReasoningEffort": null, + "contextTier": null, + "authoredContextTier": null + }, + "serialization": { + "output": "# CRLF Agent — Tester\r\n\r\n## Identity\r\n\r\n- **ID:** `crlf-agent`\r\n- **Purpose:** Edited purpose.\r\n\r\n## Accountable Responsibilities\r\n\r\n- Test CRLF.\r\n\r\n## Non-Responsibilities\r\n\r\n- LF normalization.\r\n\r\n## Collaboration and Review Authority\r\n\r\n- Review byte preservation.\r\n\r\n## X-example-data\r\n\r\nopaque: true\r\n", + "outputFixture": null, + "eol": "CRLF", + "validation": { + "accepted": true, + "conforms": true, + "classification": "canonical", + "diagnostics": [] + } + }, + "runtime": null + } + }, + { + "id": "runtime-auto-remains-selection-not-literal-override", + "capabilities": [ + "squad-charter/v0.1/parse", + "squad-charter/v0.1/validate" + ], + "input": { + "fixture": null, + "text": "# Auto Agent — Tester\n\n## Identity\n- **ID:** `auto-agent`\n- **Purpose:** Verify automatic runtime selection.\n\n## Accountable Responsibilities\n- Test runtime selection.\n\n## Non-Responsibilities\n- Select literal auto identifiers.\n\n## Collaboration and Review Authority\n- Review runtime preferences.\n\n## Model\n- **Preferred:** auto\n- **Reasoning Effort:** auto\n- **Context Tier:** auto\n", + "path": "agents/auto-agent/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": null, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": true, + "conforms": true, + "classification": "canonical", + "capabilities": ["squad-charter/v0.1/validate"], + "diagnostics": [] + }, + "parsed": { + "identity": { + "id": "auto-agent", + "purpose": "Verify automatic runtime selection.", + "name": "Auto Agent", + "role": "Tester", + "expertise": null, + "style": null + }, + "ownership": "- Test runtime selection.", + "boundaries": "- Select literal auto identifiers.", + "collaboration": "- Review runtime preferences.", + "modelPreference": "auto", + "modelRationale": null, + "modelFallback": null, + "reasoningEffort": null, + "authoredReasoningEffort": "auto", + "contextTier": null, + "authoredContextTier": "auto" + }, + "serialization": null, + "runtime": { + "compile": true, + "errorCodes": [], + "resolvedModel": null, + "resolvedReasoningEffort": null, + "resolvedContextTier": null + } + } + }, + { + "id": "conformance-api-requires-profile", + "capabilities": ["squad-charter/v0.1/validate"], + "input": { + "fixture": "positive/canonical.md", + "text": null, + "path": null, + "profile": null, + "validationApi": "conformance" + }, + "serialization": null, + "expected": { + "validation": { + "profile": "", + "accepted": false, + "conforms": false, + "classification": "invalid", + "capabilities": [], + "diagnostics": [ + {"code": "SQC014", "severity": "error", "start": {"line": 1, "column": 1}, "end": {"line": 1, "column": 1}, "path": null} + ] + }, + "parsed": null, + "serialization": null, + "runtime": null + } + }, + { + "id": "convenience-validator-defaults-to-v0.1", + "capabilities": ["squad-charter/v0.1/validate"], + "input": { + "fixture": "positive/canonical.md", + "text": null, + "path": null, + "profile": null, + "validationApi": "convenience" + }, + "serialization": null, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": true, + "conforms": true, + "classification": "canonical", + "capabilities": ["squad-charter/v0.1/validate"], + "diagnostics": [] + }, + "parsed": null, + "serialization": null, + "runtime": null + } + }, + { + "id": "unsupported-profile-fails-closed", + "capabilities": ["squad-charter/v0.1/validate"], + "input": { + "fixture": "positive/canonical.md", + "text": null, + "path": null, + "profile": "squad-charter/v9", + "validationApi": "conformance" + }, + "serialization": null, + "expected": { + "validation": { + "profile": "squad-charter/v9", + "accepted": false, + "conforms": false, + "classification": "invalid", + "capabilities": [], + "diagnostics": [ + {"code": "SQC013", "severity": "error", "start": {"line": 1, "column": 1}, "end": {"line": 1, "column": 1}, "path": null} + ] + }, + "parsed": null, + "serialization": null, + "runtime": null + } + }, + { + "id": "posix-profile-path-matches-identity", + "capabilities": ["squad-charter/v0.1/validate"], + "input": { + "fixture": "positive/canonical.md", + "text": null, + "path": "/repo/.squad/agents/architect/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": null, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": true, + "conforms": true, + "classification": "canonical", + "capabilities": ["squad-charter/v0.1/validate"], + "diagnostics": [] + }, + "parsed": null, + "serialization": null, + "runtime": null + } + }, + { + "id": "windows-profile-path-matches-identity", + "capabilities": ["squad-charter/v0.1/validate"], + "input": { + "fixture": "positive/canonical.md", + "text": null, + "path": "C:\\repo\\.squad\\agents\\architect\\charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": null, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": true, + "conforms": true, + "classification": "canonical", + "capabilities": ["squad-charter/v0.1/validate"], + "diagnostics": [] + }, + "parsed": null, + "serialization": null, + "runtime": null + } + }, + { + "id": "profile-path-mismatch", + "capabilities": ["squad-charter/v0.1/validate"], + "input": { + "fixture": "positive/canonical.md", + "text": null, + "path": ".squad/agents/different-agent/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": null, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": false, + "conforms": false, + "classification": "invalid", + "capabilities": ["squad-charter/v0.1/validate"], + "diagnostics": [ + {"code": "SQC010", "severity": "error", "start": {"line": 4, "column": 3}, "end": {"line": 4, "column": 9}, "path": ".squad/agents/different-agent/charter.md"} + ] + }, + "parsed": null, + "serialization": null, + "runtime": null + } + }, + { + "id": "unrelated-path-provides-no-profile-identity", + "capabilities": ["squad-charter/v0.1/validate"], + "input": { + "fixture": "positive/canonical.md", + "text": null, + "path": "docs/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": null, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": true, + "conforms": true, + "classification": "canonical", + "capabilities": ["squad-charter/v0.1/validate"], + "diagnostics": [] + }, + "parsed": null, + "serialization": null, + "runtime": null } }, { - "path": "legacy/template-style.md", + "id": "all-machine-field-duplicates", + "capabilities": ["squad-charter/v0.1/validate"], + "input": { + "fixture": null, + "text": "# Duplicate Agent — Tester\n\n## Identity\n- **ID:** `duplicate-agent`\n- **ID:** `duplicate-agent`\n- **Purpose:** First.\n- **Purpose:** Second.\n- **Name:** One\n- **Name:** Two\n- **Role:** Tester\n- **Role:** Reviewer\n- **Expertise:** testing\n- **Expertise:** review\n- **Style:** Direct\n- **Style:** Careful\n\n## Accountable Responsibilities\n- Test duplicates.\n\n## Non-Responsibilities\n- Production use.\n\n## Collaboration and Review Authority\n- Review diagnostics.\n\n## Model\n- **Preferred:** auto\n- **Preferred:** model\n- **Rationale:** First.\n- **Rationale:** Second.\n- **Fallback:** first\n- **Fallback:** second\n- **Reasoning Effort:** high\n- **Reasoning Effort:** low\n- **Context Tier:** default\n- **Context Tier:** long_context\n", + "path": "agents/duplicate-agent/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": null, "expected": { - "accepted": true, - "conforms": false, - "conformance": "noncanonical-compatible", - "diagnostics": [ - { "code": "SQC104", "severity": "warning", "line": 1, "column": 1 }, - { "code": "SQC104", "severity": "warning", "line": 1, "column": 1 }, - { "code": "SQC104", "severity": "warning", "line": 1, "column": 1 }, - { "code": "SQC102", "severity": "warning", "line": 3, "column": 1 }, - { "code": "SQC103", "severity": "warning", "line": 3, "column": 1 }, - { "code": "SQC105", "severity": "warning", "line": 9, "column": 1 } - ], - "identityName": "Legacy Agent", - "identityRole": "Developer" + "validation": { + "profile": "squad-charter/v0.1", + "accepted": false, + "conforms": false, + "classification": "invalid", + "capabilities": ["squad-charter/v0.1/validate"], + "diagnostics": [ + {"code": "SQC004", "severity": "error", "start": {"line": 5, "column": 3}, "end": {"line": 5, "column": 9}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC004", "severity": "error", "start": {"line": 7, "column": 3}, "end": {"line": 7, "column": 14}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC107", "severity": "warning", "start": {"line": 8, "column": 3}, "end": {"line": 8, "column": 11}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC004", "severity": "error", "start": {"line": 9, "column": 3}, "end": {"line": 9, "column": 11}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC107", "severity": "warning", "start": {"line": 9, "column": 3}, "end": {"line": 9, "column": 11}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC107", "severity": "warning", "start": {"line": 10, "column": 3}, "end": {"line": 10, "column": 11}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC004", "severity": "error", "start": {"line": 11, "column": 3}, "end": {"line": 11, "column": 11}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC107", "severity": "warning", "start": {"line": 11, "column": 3}, "end": {"line": 11, "column": 11}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC107", "severity": "warning", "start": {"line": 12, "column": 3}, "end": {"line": 12, "column": 16}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC004", "severity": "error", "start": {"line": 13, "column": 3}, "end": {"line": 13, "column": 16}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC107", "severity": "warning", "start": {"line": 13, "column": 3}, "end": {"line": 13, "column": 16}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC107", "severity": "warning", "start": {"line": 14, "column": 3}, "end": {"line": 14, "column": 12}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC004", "severity": "error", "start": {"line": 15, "column": 3}, "end": {"line": 15, "column": 12}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC107", "severity": "warning", "start": {"line": 15, "column": 3}, "end": {"line": 15, "column": 12}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC004", "severity": "error", "start": {"line": 28, "column": 3}, "end": {"line": 28, "column": 16}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC004", "severity": "error", "start": {"line": 30, "column": 3}, "end": {"line": 30, "column": 16}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC107", "severity": "warning", "start": {"line": 31, "column": 3}, "end": {"line": 31, "column": 15}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC004", "severity": "error", "start": {"line": 32, "column": 3}, "end": {"line": 32, "column": 15}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC107", "severity": "warning", "start": {"line": 32, "column": 3}, "end": {"line": 32, "column": 15}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC004", "severity": "error", "start": {"line": 34, "column": 3}, "end": {"line": 34, "column": 23}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC004", "severity": "error", "start": {"line": 36, "column": 3}, "end": {"line": 36, "column": 19}, "path": "agents/duplicate-agent/charter.md"} + ] + }, + "parsed": null, + "serialization": null, + "runtime": null } }, { - "path": "round-trip/extensions.md", + "id": "duplicate-structural-identities-and-sections", + "capabilities": ["squad-charter/v0.1/validate"], + "input": { + "fixture": null, + "text": "# Duplicate Agent — Tester\n# Second Identity — Tester\n\n## Identity\n- **ID:** `duplicate-agent`\n- **Purpose:** Duplicate structure.\n\n## Identity\n\n## Accountable Responsibilities\n- First.\n\n## What I Own\n- Duplicate alias.\n\n## Non-Responsibilities\n- First.\n\n## Boundaries\n- Duplicate alias.\n\n## Collaboration and Review Authority\n- First.\n\n## Collaboration\n- Duplicate alias.\n\n## Model\n- **Preferred:** auto\n\n## Model\n- **Preferred:** auto\n", + "path": "agents/duplicate-agent/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": null, "expected": { - "accepted": true, - "conforms": true, - "conformance": "canonical", - "diagnostics": [], - "identityId": "extensible-agent", - "serializedOutputPath": "round-trip/extensions.md", - "editedOutputPath": "round-trip/extensions.edited.md" + "validation": { + "profile": "squad-charter/v0.1", + "accepted": false, + "conforms": false, + "classification": "invalid", + "capabilities": ["squad-charter/v0.1/validate"], + "diagnostics": [ + {"code": "SQC015", "severity": "error", "start": {"line": 2, "column": 1}, "end": {"line": 2, "column": 26}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC011", "severity": "error", "start": {"line": 8, "column": 1}, "end": {"line": 8, "column": 11}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC011", "severity": "error", "start": {"line": 13, "column": 1}, "end": {"line": 13, "column": 13}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC105", "severity": "warning", "start": {"line": 13, "column": 1}, "end": {"line": 13, "column": 13}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC011", "severity": "error", "start": {"line": 19, "column": 1}, "end": {"line": 19, "column": 13}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC011", "severity": "error", "start": {"line": 25, "column": 1}, "end": {"line": 25, "column": 16}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC011", "severity": "error", "start": {"line": 31, "column": 1}, "end": {"line": 31, "column": 8}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC004", "severity": "error", "start": {"line": 32, "column": 3}, "end": {"line": 32, "column": 16}, "path": "agents/duplicate-agent/charter.md"} + ] + }, + "parsed": null, + "serialization": null, + "runtime": null + } + }, + { + "id": "extension-grammar-and-case-collision", + "capabilities": ["squad-charter/v0.1/validate"], + "input": { + "fixture": null, + "text": "# Extension Agent — Tester\n\n## Identity\n- **ID:** `extension-agent`\n- **Purpose:** Validate extension ownership grammar.\n\n## Accountable Responsibilities\n- Test extensions.\n\n## Non-Responsibilities\n- Interpret opaque bytes.\n\n## Collaboration and Review Authority\n- Review collisions.\n\n## X-Example-data\none\n\n## x-example-data\ntwo\n\n## X-example\nthree\n", + "path": "agents/extension-agent/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": null, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": false, + "conforms": false, + "classification": "invalid", + "capabilities": ["squad-charter/v0.1/validate"], + "diagnostics": [ + {"code": "SQC106", "severity": "warning", "start": {"line": 16, "column": 1}, "end": {"line": 16, "column": 17}, "path": "agents/extension-agent/charter.md"}, + {"code": "SQC012", "severity": "error", "start": {"line": 19, "column": 1}, "end": {"line": 19, "column": 17}, "path": "agents/extension-agent/charter.md"}, + {"code": "SQC106", "severity": "warning", "start": {"line": 19, "column": 1}, "end": {"line": 19, "column": 17}, "path": "agents/extension-agent/charter.md"}, + {"code": "SQC106", "severity": "warning", "start": {"line": 22, "column": 1}, "end": {"line": 22, "column": 12}, "path": "agents/extension-agent/charter.md"} + ] + }, + "parsed": null, + "serialization": null, + "runtime": null + } + }, + { + "id": "reserved-squad-extension-is-mechanically-valid", + "capabilities": ["squad-charter/v0.1/validate"], + "input": { + "fixture": null, + "text": "# Extension Agent — Tester\n\n## Identity\n- **ID:** `extension-agent`\n- **Purpose:** Validate reserved extension syntax.\n\n## Accountable Responsibilities\n- Test extensions.\n\n## Non-Responsibilities\n- Claim Squad namespace ownership.\n\n## Collaboration and Review Authority\n- Review extension syntax.\n\n## X-squad-policy\nopaque: true\n", + "path": "agents/extension-agent/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": null, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": true, + "conforms": true, + "classification": "canonical", + "capabilities": ["squad-charter/v0.1/validate"], + "diagnostics": [] + }, + "parsed": null, + "serialization": null, + "runtime": null + } + }, + { + "id": "noncanonical-h1-separator", + "capabilities": ["squad-charter/v0.1/validate"], + "input": { + "fixture": null, + "text": "# Separator Agent - Tester\n\n## Identity\n- **ID:** `separator-agent`\n- **Purpose:** Validate the canonical separator.\n\n## Accountable Responsibilities\n- Test headings.\n\n## Non-Responsibilities\n- Accept hyphen as canonical.\n\n## Collaboration and Review Authority\n- Review headings.\n", + "path": "agents/separator-agent/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": null, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": true, + "conforms": false, + "classification": "noncanonical-compatible", + "capabilities": ["squad-charter/v0.1/validate"], + "diagnostics": [ + {"code": "SQC003", "severity": "warning", "start": {"line": 1, "column": 1}, "end": {"line": 1, "column": 26}, "path": "agents/separator-agent/charter.md"} + ] + }, + "parsed": null, + "serialization": null, + "runtime": null + } + }, + { + "id": "noncanonical-standard-section-order", + "capabilities": ["squad-charter/v0.1/validate"], + "input": { + "fixture": null, + "text": "# Ordered Agent — Tester\n\n## Identity\n- **ID:** `ordered-agent`\n- **Purpose:** Validate section order.\n\n## Accountable Responsibilities\n- Test ordering.\n\n## Non-Responsibilities\n- Reorder silently.\n\n## Model\n- **Preferred:** auto\n\n## Collaboration and Review Authority\n- Review ordering.\n", + "path": "agents/ordered-agent/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": null, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": true, + "conforms": false, + "classification": "noncanonical-compatible", + "capabilities": ["squad-charter/v0.1/validate"], + "diagnostics": [ + {"code": "SQC108", "severity": "warning", "start": {"line": 16, "column": 1}, "end": {"line": 16, "column": 37}, "path": "agents/ordered-agent/charter.md"} + ] + }, + "parsed": null, + "serialization": null, + "runtime": null } } - ], - "duplicateFields": [ - { "section": "Identity", "name": "ID", "value": "`duplicate-agent`" }, - { "section": "Identity", "name": "Purpose", "value": "Duplicate purpose." }, - { "section": "Identity", "name": "Name", "value": "Duplicate Agent" }, - { "section": "Identity", "name": "Role", "value": "Tester" }, - { "section": "Identity", "name": "Expertise", "value": "testing" }, - { "section": "Identity", "name": "Style", "value": "Direct" }, - { "section": "Model", "name": "Preferred", "value": "auto" }, - { "section": "Model", "name": "Rationale", "value": "Testing." }, - { "section": "Model", "name": "Fallback", "value": "fallback-model" }, - { "section": "Model", "name": "Reasoning Effort", "value": "auto" }, - { "section": "Model", "name": "Context Tier", "value": "auto" } - ], - "duplicateSections": [ - "Identity", - "Accountable Responsibilities", - "Non-Responsibilities", - "Collaboration and Review Authority", - "Model" ] } diff --git a/test/charter-compiler.test.ts b/test/charter-compiler.test.ts index e2736a1bf..af13230d5 100644 --- a/test/charter-compiler.test.ts +++ b/test/charter-compiler.test.ts @@ -355,15 +355,27 @@ describe('compileCharterFull', () => { expect(result.prompt).toContain('Fenster, Verbal, Hockney'); }); - it('handles empty charter content gracefully', () => { - const result = compileCharterFull({ + it('rejects invalid charter content before applying behavior', () => { + expect(() => compileCharterFull({ agentName: 'empty', charterPath: '/test/charter.md', charterContent: '', + })).toThrow(/SQC001/); + }); + + it('returns the validation evidence used before applying behavior', () => { + const result = compileCharterFull({ + agentName: 'verbal', + charterPath: '/test/charter.md', + charterContent: FULL_CHARTER, }); - expect(result.name).toBe('empty'); - expect(result.prompt).toContain('empty'); + expect(result.validation).toMatchObject({ + profile: 'squad-charter/v0.1', + accepted: true, + conforms: false, + conformance: 'noncanonical-compatible', + }); }); it('description includes expertise when available', () => { diff --git a/test/charter-spec.test.ts b/test/charter-spec.test.ts index 510f36385..1ce09cc2a 100644 --- a/test/charter-spec.test.ts +++ b/test/charter-spec.test.ts @@ -8,44 +8,98 @@ import { compileCharterFull, parseCharterMarkdown, serializeCharter, + validateCharterConformance, validateCharterMarkdown, - type CharterConformance, - type CharterDiagnosticCode, - type CharterDiagnosticSeverity, + type CharterCapability, + type CharterDiagnostic, + type ParsedCharter, } from '@bradygaster/squad-sdk/parsers'; -interface ExpectedDiagnostic { - code: CharterDiagnosticCode; - severity: CharterDiagnosticSeverity; - line: number; - column: number; +type ValidationApi = 'conformance' | 'convenience'; +type SerializationMode = 'canonical' | 'legacy' | 'preserve'; + +interface ManifestDiagnostic { + code: string; + severity: 'error' | 'warning'; + start: { line: number; column: number }; + end: { line: number; column: number }; + path: string | null; } -interface FixtureManifest { - profile: string; - requiredCapabilities: string[]; - fixtures: Array<{ - path: string; - expected: { +interface ParsedSemantics { + identity: { + id: string | null; + purpose: string | null; + name: string | null; + role: string | null; + expertise: string[] | null; + style: string | null; + }; + ownership: string | null; + boundaries: string | null; + collaboration: string | null; + modelPreference: string | null; + modelRationale: string | null; + modelFallback: string | null; + reasoningEffort: string | null; + authoredReasoningEffort: string | null; + contextTier: string | null; + authoredContextTier: string | null; +} + +interface ManifestCase { + id: string; + capabilities: CharterCapability[]; + input: { + fixture: string | null; + text: string | null; + path: string | null; + profile: string | null; + validationApi: ValidationApi; + }; + serialization: { + mode: SerializationMode; + edits: Array<{ + field: 'identity.purpose'; + value: string; + }>; + } | null; + expected: { + validation: { + profile: string; accepted: boolean; conforms: boolean; - conformance: CharterConformance; - diagnostics: ExpectedDiagnostic[]; - identityId?: string; - identityName?: string; - identityRole?: string; - reasoningEffort?: string; - contextTier?: string; - serializedOutputPath?: string; - editedOutputPath?: string; + classification: 'canonical' | 'noncanonical-compatible' | 'invalid'; + capabilities: CharterCapability[]; + diagnostics: ManifestDiagnostic[]; }; - }>; - duplicateFields: Array<{ - section: 'Identity' | 'Model'; - name: string; - value: string; - }>; - duplicateSections: string[]; + parsed: ParsedSemantics | null; + serialization: { + output: string | null; + outputFixture: string | null; + eol: 'LF' | 'CRLF' | 'CR'; + validation: { + accepted: boolean; + conforms: boolean; + classification: 'canonical' | 'noncanonical-compatible' | 'invalid'; + diagnostics: ManifestDiagnostic[]; + }; + } | null; + runtime: { + compile: boolean; + errorCodes: string[]; + resolvedModel: string | null; + resolvedReasoningEffort: string | null; + resolvedContextTier: string | null; + } | null; + }; +} + +interface FixtureManifest { + schemaVersion: 1; + profile: string; + capabilities: CharterCapability[]; + cases: ManifestCase[]; } const FIXTURE_ROOT = path.resolve(__dirname, '..', 'test-fixtures', 'spec', 'charter-v0.1'); @@ -56,33 +110,80 @@ async function loadManifest(): Promise { ) as FixtureManifest; } -const CANONICAL = `# Contract Agent — Tester +async function readCaseSource(testCase: ManifestCase): Promise { + if (testCase.input.fixture !== null) { + return readFile(path.join(FIXTURE_ROOT, testCase.input.fixture), 'utf8'); + } + if (testCase.input.text !== null) return testCase.input.text; + throw new Error(`${testCase.id}: exactly one input source is required`); +} -## Identity -- **ID:** \`contract-agent\` -- **Purpose:** Exercise the charter contract. +function validateCase(testCase: ManifestCase, markdown: string) { + const options = { + ...(testCase.input.path === null ? {} : { path: testCase.input.path }), + ...(testCase.input.profile === null ? {} : { profile: testCase.input.profile }), + }; + return testCase.input.validationApi === 'conformance' + ? Reflect.apply(validateCharterConformance, undefined, [markdown, options]) + : validateCharterMarkdown(markdown, options); +} -## Accountable Responsibilities -- Contract tests. +function normalizeDiagnostics( + diagnostics: readonly CharterDiagnostic[], +): ManifestDiagnostic[] { + return diagnostics.map(diagnostic => ({ + code: diagnostic.code, + severity: diagnostic.severity, + start: { line: diagnostic.line, column: diagnostic.column }, + end: { line: diagnostic.endLine, column: diagnostic.endColumn }, + path: diagnostic.path ?? null, + })); +} -## Non-Responsibilities -- Runtime authorization. +function normalizeParsed(parsed: ParsedCharter): ParsedSemantics { + return { + identity: { + id: parsed.identity.id ?? null, + purpose: parsed.identity.purpose ?? null, + name: parsed.identity.name ?? null, + role: parsed.identity.role ?? null, + expertise: parsed.identity.expertise ?? null, + style: parsed.identity.style ?? null, + }, + ownership: parsed.ownership ?? null, + boundaries: parsed.boundaries ?? null, + collaboration: parsed.collaboration ?? null, + modelPreference: parsed.modelPreference ?? null, + modelRationale: parsed.modelRationale ?? null, + modelFallback: parsed.modelFallback ?? null, + reasoningEffort: parsed.reasoningEffort ?? null, + authoredReasoningEffort: parsed.authoredReasoningEffort ?? null, + contextTier: parsed.contextTier ?? null, + authoredContextTier: parsed.authoredContextTier ?? null, + }; +} -## Collaboration and Review Authority -- Reviews public API compatibility. +function applyEdits(parsed: ParsedCharter, testCase: ManifestCase): void { + for (const edit of testCase.serialization?.edits ?? []) { + if (edit.field === 'identity.purpose') { + parsed.identity.purpose = edit.value; + } + } +} -## Model -- **Preferred:** auto -- **Reasoning Effort:** auto -- **Context Tier:** auto -`; +function eolName(value: string): 'LF' | 'CRLF' | 'CR' { + const first = value.match(/\r\n|\r|\n/)?.[0]; + if (first === '\r\n') return 'CRLF'; + if (first === '\r') return 'CR'; + return 'LF'; +} -describe('Squad Charter Profile v0.1', () => { - it('executes the language-neutral conformance manifest', async () => { +describe('Squad Charter Profile v0.1 portable conformance manifest', () => { + it('declares the published profile metadata and every operational capability', async () => { const manifest = await loadManifest(); - + expect(manifest.schemaVersion).toBe(1); expect(manifest.profile).toBe(CHARTER_PROFILE); - expect(manifest.requiredCapabilities).toEqual(Object.values(CHARTER_CAPABILITIES)); + expect(manifest.capabilities).toEqual(Object.values(CHARTER_CAPABILITIES)); expect(CHARTER_PROFILE_METADATA).toEqual({ id: CHARTER_PROFILE, artifactClass: 'required', @@ -90,308 +191,97 @@ describe('Squad Charter Profile v0.1', () => { repositoryRelativePath: '.squad/agents/{id}/charter.md', }); - for (const fixture of manifest.fixtures) { - const fixturePath = path.join(FIXTURE_ROOT, fixture.path); - const markdown = await readFile(fixturePath, 'utf8'); - const validation = validateCharterMarkdown(markdown, { path: fixture.path }); - const parsed = parseCharterMarkdown(markdown); - - expect(validation.accepted, fixture.path).toBe(fixture.expected.accepted); - expect(validation.conforms, fixture.path).toBe(fixture.expected.conforms); - expect(validation.conformance, fixture.path).toBe(fixture.expected.conformance); - expect(validation.capabilities, fixture.path).toEqual([CHARTER_CAPABILITIES.validate]); + for (const capability of manifest.capabilities) { expect( - validation.diagnostics.map(({ code, severity, line, column }) => ({ - code, - severity, - line, - column, - })), - fixture.path, - ).toEqual(fixture.expected.diagnostics); - - if (fixture.expected.identityId) { - expect(parsed.identity.id, fixture.path).toBe(fixture.expected.identityId); - } - if (fixture.expected.identityName) { - expect(parsed.identity.name, fixture.path).toBe(fixture.expected.identityName); - } - if (fixture.expected.identityRole) { - expect(parsed.identity.role, fixture.path).toBe(fixture.expected.identityRole); - } - if (fixture.expected.reasoningEffort) { - expect(parsed.reasoningEffort, fixture.path).toBe(fixture.expected.reasoningEffort); - } - if (fixture.expected.contextTier) { - expect(parsed.contextTier, fixture.path).toBe(fixture.expected.contextTier); - } + manifest.cases.some(testCase => testCase.capabilities.includes(capability)), + capability, + ).toBe(true); } }); - it('preserves exact source on canonical no-op and exact extensions after a known edit', async () => { + it('executes every normative case without synthesizing cases in TypeScript', async () => { const manifest = await loadManifest(); - const fixture = manifest.fixtures.find(item => item.expected.editedOutputPath); - expect(fixture).toBeDefined(); - - const markdown = await readFile(path.join(FIXTURE_ROOT, fixture!.path), 'utf8'); - const expectedNoOp = await readFile( - path.join(FIXTURE_ROOT, fixture!.expected.serializedOutputPath!), - 'utf8', - ); - const expectedEdited = await readFile( - path.join(FIXTURE_ROOT, fixture!.expected.editedOutputPath!), - 'utf8', - ); - const parsed = parseCharterMarkdown(markdown); - - expect(serializeCharter(parsed)).toBe(expectedNoOp); - - parsed.identity.purpose = 'Verify edited fields without losing unknown extensions.'; - const edited = serializeCharter(parsed); - expect(edited).toBe(expectedEdited); - expect(edited.indexOf('## X-example-deployment')) - .toBeLessThan(edited.indexOf('## X-example-policy')); - }); - - it('uses one scanner that ignores structural lookalikes in opaque contexts', async () => { - const markdown = await readFile( - path.join(FIXTURE_ROOT, 'round-trip', 'extensions.md'), - 'utf8', - ); - const parsed = parseCharterMarkdown(markdown); - const validation = validateCharterMarkdown(markdown); - - expect(parsed.identity.id).toBe('extensible-agent'); - expect(parsed.modelPreference).toBeUndefined(); - expect(parsed.reasoningEffort).toBeUndefined(); - expect(parsed.contextTier).toBeUndefined(); - expect(validation).toMatchObject({ - accepted: true, - conforms: true, - conformance: 'canonical', - diagnostics: [], - }); - }); - it('preserves CRLF source exactly and preserves CRLF extension bytes after editing', () => { - const markdown = CANONICAL.replace(/\n/g, '\r\n') - + '\r\n## X-example-data\r\n\r\nopaque: true\r\n'; - const parsed = parseCharterMarkdown(markdown); - expect(serializeCharter(parsed)).toBe(markdown); - - parsed.identity.purpose = 'Edited purpose.'; - const edited = serializeCharter(parsed); - expect(edited).toContain('## X-example-data\r\n\r\nopaque: true\r\n'); - expect(validateCharterMarkdown(edited).accepted).toBe(true); - }); - - it('enforces profile identity for POSIX and Windows paths only', () => { - expect( - validateCharterMarkdown(CANONICAL, { - path: '/repo/.squad/agents/contract-agent/charter.md', - }).diagnostics, - ).toEqual([]); - expect( - validateCharterMarkdown(CANONICAL, { - path: 'C:\\repo\\.squad\\agents\\contract-agent\\charter.md', - }).diagnostics, - ).toEqual([]); - expect( - validateCharterMarkdown(CANONICAL, { - path: 'agents/contract-agent/charter.md', - }).diagnostics, - ).toEqual([]); - - const mismatch = validateCharterMarkdown(CANONICAL, { - path: '.squad/agents/different-agent/charter.md', - }); - expect(mismatch.diagnostics.map(diagnostic => diagnostic.code)).toEqual(['SQC010']); - expect(mismatch.diagnostics[0]).toMatchObject({ line: 4, column: 3 }); - - expect( - validateCharterMarkdown(CANONICAL, { path: 'docs/charter.md' }).diagnostics, - ).toEqual([]); - }); - - it('rejects every duplicate standard machine field', async () => { - const manifest = await loadManifest(); - for (const duplicate of manifest.duplicateFields) { - const markdown = charterWithDuplicateField(duplicate); - const diagnostics = validateCharterMarkdown(markdown).diagnostics; - expect( - diagnostics.filter(diagnostic => diagnostic.code === 'SQC004'), - `${duplicate.section}.${duplicate.name}`, - ).toHaveLength(1); - } - }); - - it('rejects duplicate standard sections, including semantic legacy aliases', async () => { - const manifest = await loadManifest(); - for (const section of manifest.duplicateSections) { - const markdown = `${CANONICAL}\n## ${section}\n`; + for (const testCase of manifest.cases) { + const markdown = await readCaseSource(testCase); + const validation = validateCase(testCase, markdown); expect( - validateCharterMarkdown(markdown).diagnostics.some( - diagnostic => diagnostic.code === 'SQC011', - ), - section, - ).toBe(true); - } - - const aliasDuplicate = CANONICAL.replace( - '## Non-Responsibilities', - '## Boundaries\n\nLegacy duplicate.\n\n## Non-Responsibilities', - ); - expect( - validateCharterMarkdown(aliasDuplicate).diagnostics.some( - diagnostic => diagnostic.code === 'SQC011', - ), - ).toBe(true); - }); - - function charterWithDuplicateField( - duplicate: FixtureManifest['duplicateFields'][number], - ): string { - const duplicateLines = [ - `- **${duplicate.name}:** ${duplicate.value}`, - `- **${duplicate.name}:** ${duplicate.value}`, - ].join('\n'); - const identityFields = duplicate.section === 'Identity' - ? [ - ...(duplicate.name === 'ID' ? [] : ['- **ID:** `contract-agent`']), - ...(duplicate.name === 'Purpose' ? [] : ['- **Purpose:** Exercise the charter contract.']), - duplicateLines, - ].join('\n') - : [ - '- **ID:** `contract-agent`', - '- **Purpose:** Exercise the charter contract.', - ].join('\n'); - const modelFields = duplicate.section === 'Model' - ? duplicateLines - : [ - '- **Preferred:** auto', - '- **Reasoning Effort:** auto', - '- **Context Tier:** auto', - ].join('\n'); - - return [ - '# Contract Agent — Tester', - '', - '## Identity', - identityFields, - '', - '## Accountable Responsibilities', - '- Contract tests.', - '', - '## Non-Responsibilities', - '- Runtime authorization.', - '', - '## Collaboration and Review Authority', - '- Reviews public API compatibility.', - '', - '## Model', - modelFields, - '', - ].join('\n'); - } - - it('emits canonical responsibility headings unless legacy mode is explicit', async () => { - const legacy = await readFile( - path.join(FIXTURE_ROOT, 'legacy', 'template-style.md'), - 'utf8', - ); - const parsed = parseCharterMarkdown(legacy); - const canonical = serializeCharter(parsed); - - expect(canonical).toContain('## Accountable Responsibilities'); - expect(canonical).toContain('## Non-Responsibilities'); - expect(canonical).toContain('## Collaboration and Review Authority'); - expect(canonical).not.toContain('## What I Own'); - expect(serializeCharter(parsed, { format: 'legacy' })).toContain('## What I Own'); - expect(serializeCharter(parsed, { format: 'preserve' })).toBe(legacy); - }); - - it('preserves authored auto preferences without producing runtime overrides', () => { - const compiled = compileCharterFull({ - agentName: 'contract-agent', - charterPath: '.squad/agents/contract-agent/charter.md', - charterContent: CANONICAL, - }); - - expect(compiled.parsed.modelPreference).toBe('auto'); - expect(compiled.parsed.authoredReasoningEffort).toBe('auto'); - expect(compiled.parsed.authoredContextTier).toBe('auto'); - expect(compiled.resolvedModel).toBeUndefined(); - expect(compiled.resolvedReasoningEffort).toBeUndefined(); - expect(compiled.resolvedContextTier).toBeUndefined(); - expect(serializeCharter(compiled.parsed)).toBe(CANONICAL); - - const explicitAutoOverride = compileCharterFull({ - agentName: 'contract-agent', - charterPath: '.squad/agents/contract-agent/charter.md', - charterContent: CANONICAL - .replace('**Preferred:** auto', '**Preferred:** supported-model') - .replace('**Reasoning Effort:** auto', '**Reasoning Effort:** high') - .replace('**Context Tier:** auto', '**Context Tier:** long_context'), - configOverrides: { - model: 'auto', - reasoningEffort: 'auto', - contextTier: 'auto', - }, - }); - expect(explicitAutoOverride.resolvedModel).toBeUndefined(); - expect(explicitAutoOverride.resolvedReasoningEffort).toBeUndefined(); - expect(explicitAutoOverride.resolvedContextTier).toBeUndefined(); - }); - - it('reports unsupported profiles and extension collisions deterministically', () => { - const unsupported = validateCharterMarkdown(CANONICAL, { - profile: 'squad-charter/v9', - }); - expect(unsupported).toMatchObject({ - accepted: false, - conforms: false, - conformance: 'invalid', - capabilities: [], - }); - expect(unsupported.diagnostics.map(diagnostic => diagnostic.code)).toEqual(['SQC013']); - - const extensions = `${CANONICAL} -## X-Example-Data - -one - -## x-example-data - -two -`; - expect( - validateCharterMarkdown(extensions).diagnostics.map( - diagnostic => diagnostic.code, - ), - ).toEqual(['SQC106', 'SQC012', 'SQC106']); - }); - - it('returns identical diagnostic ordering and ranges across runs', () => { - const markdown = `# Invalid Agent - -## Identity -- **ID:** Invalid Agent -- **Purpose:** -- **Purpose:** duplicate + { + profile: validation.profile, + accepted: validation.accepted, + conforms: validation.conforms, + classification: validation.conformance, + capabilities: validation.capabilities, + diagnostics: normalizeDiagnostics(validation.diagnostics), + }, + testCase.id, + ).toEqual(testCase.expected.validation); + + if (testCase.expected.parsed !== null) { + expect( + normalizeParsed(parseCharterMarkdown(markdown)), + `${testCase.id}: parsed semantics`, + ).toEqual(testCase.expected.parsed); + } -## Model -- **Context Tier:** huge -- **Reasoning Effort:** turbo -`; - const first = validateCharterMarkdown(markdown); - const second = validateCharterMarkdown(markdown); + if (testCase.serialization !== null) { + const parsed = parseCharterMarkdown(markdown); + applyEdits(parsed, testCase); + const output = serializeCharter(parsed, { format: testCase.serialization.mode }); + const expectedOutput = testCase.expected.serialization?.outputFixture + ? await readFile( + path.join(FIXTURE_ROOT, testCase.expected.serialization.outputFixture), + 'utf8', + ) + : testCase.expected.serialization?.output; + expect(output, `${testCase.id}: serialized output`).toBe(expectedOutput); + expect(eolName(output), `${testCase.id}: line endings`) + .toBe(testCase.expected.serialization?.eol); + const outputValidation = validateCharterConformance(output, { + profile: manifest.profile, + ...(testCase.input.path === null ? {} : { path: testCase.input.path }), + }); + expect({ + accepted: outputValidation.accepted, + conforms: outputValidation.conforms, + classification: outputValidation.conformance, + diagnostics: normalizeDiagnostics(outputValidation.diagnostics), + }, `${testCase.id}: serialized output validation`) + .toEqual(testCase.expected.serialization?.validation); + } - expect(second).toEqual(first); - expect(first.diagnostics.every(diagnostic => - diagnostic.line >= 1 - && diagnostic.column >= 1 - && diagnostic.endLine >= diagnostic.line - && diagnostic.endColumn >= diagnostic.column, - )).toBe(true); + if (testCase.expected.runtime !== null) { + const compile = () => compileCharterFull({ + agentName: 'manifest-agent', + charterPath: testCase.input.path ?? 'agents/manifest-agent/charter.md', + charterContent: markdown, + ...(testCase.input.profile === null ? {} : { profile: testCase.input.profile }), + }); + + if (!testCase.expected.runtime.compile) { + let message = ''; + try { + compile(); + } catch (error) { + message = error instanceof Error ? error.message : String(error); + } + expect(message, `${testCase.id}: runtime rejection`).not.toBe(''); + for (const code of testCase.expected.runtime.errorCodes) { + expect(message, `${testCase.id}: ${code}`).toContain(code); + } + } else { + const compiled = compile(); + expect({ + resolvedModel: compiled.resolvedModel ?? null, + resolvedReasoningEffort: compiled.resolvedReasoningEffort ?? null, + resolvedContextTier: compiled.resolvedContextTier ?? null, + }, `${testCase.id}: runtime semantics`).toEqual({ + resolvedModel: testCase.expected.runtime.resolvedModel, + resolvedReasoningEffort: testCase.expected.runtime.resolvedReasoningEffort, + resolvedContextTier: testCase.expected.runtime.resolvedContextTier, + }); + } + } + } }); }); diff --git a/test/config-integration.test.ts b/test/config-integration.test.ts index b3b639abd..8ca356750 100644 --- a/test/config-integration.test.ts +++ b/test/config-integration.test.ts @@ -462,15 +462,12 @@ describe('Integration: Charter compilation with config overrides', () => { expect(result.prompt).toContain('ADR-001: Use TypeScript.'); }); - it('handles empty charter content gracefully', () => { - const result = compileCharter({ + it('rejects explicitly empty charter content', () => { + expect(() => compileCharter({ agentName: 'empty', charterPath: '/fake/path', charterContent: '', - }); - - expect(result.name).toBe('empty'); - expect(result.prompt).toContain('empty Charter'); + })).toThrow(/SQC001/); }); it('compileCharterFull returns parsed charter', () => { @@ -895,8 +892,7 @@ describe('Integration: Full pipeline — discover → compile → resolve → fi expect(agent).toBeNull(); - // Compilation with empty charter content is still valid - const compiled = compileCharter({ + expect(() => compileCharter({ agentName: 'phantom', charterPath: join(base, '.squad', 'agents', 'phantom', 'charter.md'), charterContent: '', @@ -904,10 +900,7 @@ describe('Integration: Full pipeline — discover → compile → resolve → fi role: 'Ghost Agent', model: 'claude-haiku-4.5', }, - }); - - expect(compiled.name).toBe('phantom'); - expect(compiled.displayName).toContain('Ghost Agent'); + })).toThrow(/SQC001/); }); it('ModelRegistry validates models from compiled config', () => { From f7d81a4722a5c6959252951c19da8bfe1cabbb09 Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Thu, 24 Sep 2026 16:26:29 -0700 Subject: [PATCH 03/11] fix: complete charter conformance gate Part of #2069 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Source-Commit: f7d430a78e38dc00496f1799d76398df0f2a4884 --- .changeset/formalize-charter-profile.md | 2 +- docs/specification/README.md | 11 +- docs/specification/artifact-inventory.md | 4 +- docs/specification/charter-v0.1.md | 69 ++- docs/specification/core-v0.1.md | 26 +- packages/squad-sdk/CHANGELOG.md | 4 +- .../squad-sdk/src/agents/charter-compiler.ts | 8 +- .../squad-sdk/src/agents/charter-validator.ts | 35 +- test-fixtures/spec/charter-v0.1/manifest.json | 429 +++++++++++++- .../spec/charter-v0.1/manifest.schema.json | 558 ++++++++++++++++++ test/charter-spec.test.ts | 110 +++- 11 files changed, 1191 insertions(+), 65 deletions(-) create mode 100644 test-fixtures/spec/charter-v0.1/manifest.schema.json diff --git a/.changeset/formalize-charter-profile.md b/.changeset/formalize-charter-profile.md index 5f8e7b8ee..0e0de8705 100644 --- a/.changeset/formalize-charter-profile.md +++ b/.changeset/formalize-charter-profile.md @@ -2,4 +2,4 @@ '@bradygaster/squad-sdk': minor --- -Add the draft `squad-charter/v0.1` artifact profile, explicit conformance profile selection, validation-before-runtime compilation, typed diagnostics and capability metadata, canonical and lossless extension-preserving serialization, a language-neutral manifest-driven conformance suite, and implementation-independent specification documents. +Add the draft `squad-charter/v0.1` artifact profile, explicit conformance profile selection, validation-before-runtime compilation, typed diagnostics and operation-exact capability metadata, canonical and lossless extension-preserving serialization, a schema-defined language-neutral conformance manifest with complete diagnostic coverage, and implementation-independent specification documents. diff --git a/docs/specification/README.md b/docs/specification/README.md index 8ebbc4605..a8ec32404 100644 --- a/docs/specification/README.md +++ b/docs/specification/README.md @@ -4,6 +4,14 @@ **Status:** Working Draft +**Draft date:** 2026-09-24 + +**Working-draft provenance:** [PR #2074](https://github.com/bradygaster/squad/pull/2074), +whose revision lineage starts at immutable review baseline +[`ab80da18087b7d37755db2c3cc5880d194a10f52`](https://github.com/bradygaster/squad/commit/ab80da18087b7d37755db2c3cc5880d194a10f52). +Each subsequent draft snapshot is identified by its immutable Git commit in +that pull request; no publication tag has been assigned. + **Document class:** Informative index; linked core/profile documents identify their normative sections **First artifact profile:** [Squad Charter Profile v0.1](charter-v0.1.md) @@ -23,7 +31,8 @@ without claiming that an entire Squad is conformant. surface, parser locations, maturity, and proposed profile order. - [`test-fixtures/spec/charter-v0.1/`](../../test-fixtures/spec/charter-v0.1/) contains language-neutral positive, negative, legacy, and round-trip - fixtures. + fixtures. Its [`manifest.schema.json`](../../test-fixtures/spec/charter-v0.1/manifest.schema.json) + defines the portable manifest field contract. Normative references are maintained in [Core v0.1 §2](core-v0.1.md#2-normative-references), including BCP 14, diff --git a/docs/specification/artifact-inventory.md b/docs/specification/artifact-inventory.md index 2b04b7555..2bcbe77b9 100644 --- a/docs/specification/artifact-inventory.md +++ b/docs/specification/artifact-inventory.md @@ -41,7 +41,9 @@ responsibility sections, while the active specialists used the first. It also parsed `Reasoning Effort` and `Context Tier`, but the charter serializer omitted both fields. The reference implementation now recognizes canonical and legacy aliases, preserves both behavioral fields in serialization, and exposes a -deterministic validator. +deterministic validator plus an independently declared runtime capability. The +portable conformance manifest is governed by a language-neutral JSON Schema and +covers every normative diagnostic code. ## Profile sequencing diff --git a/docs/specification/charter-v0.1.md b/docs/specification/charter-v0.1.md index f4c5e7628..2167fd2bd 100644 --- a/docs/specification/charter-v0.1.md +++ b/docs/specification/charter-v0.1.md @@ -10,12 +10,15 @@ **Editor:** Squad project maintainers -**Publication date:** 2026-09-24 +**Draft date:** 2026-09-24 -**Source revision:** `squad-charter-v0.1-wd3` +**Working-draft provenance:** [PR #2074](https://github.com/bradygaster/squad/pull/2074), +whose revision lineage starts at immutable review baseline +[`ab80da18087b7d37755db2c3cc5880d194a10f52`](https://github.com/bradygaster/squad/commit/ab80da18087b7d37755db2c3cc5880d194a10f52). +Each subsequent draft snapshot is identified by its immutable Git commit in +that pull request. -**Immutable publication tag:** Not assigned; this is a source-controlled -working draft +**Immutable publication tag:** None; this working draft is not a publication **Feedback:** [Issue #2069](https://github.com/bradygaster/squad/issues/2069) @@ -61,10 +64,12 @@ profile version is selected by the calling API or surrounding manifest; v0.1 has no in-document version marker. An API that claims portable conformance MUST require the caller to supply the -profile identifier. Missing selection produces `SQC014`. The TypeScript -reference implementation also exposes `validateCharterMarkdown` as a -convenience API that defaults to `squad-charter/v0.1`; this behavior is not -explicit negotiation, and its result always reports the selected profile. +profile identifier. Missing selection produces `SQC014`. A language-specific +implementation MAY expose a separately named convenience validator or compiler +that defaults a profile, but that default is implementation-specific and MUST +NOT be treated as portable conformance behavior. The TypeScript reference +implementation's `validateCharterMarkdown` and `compileCharterFull` convenience +paths default to `squad-charter/v0.1` and report the selected profile. A consumer asked to process a profile other than one it supports MUST return an unsupported-profile error and MUST NOT reinterpret the document as v0.1. @@ -154,6 +159,8 @@ are accepted for legacy ingestion but make the document **noncanonical-compatible**. Editors preserve them when their canonical semantics cannot be represented without data loss. They are not behavioral inputs except that `Name` and `Role` may supply legacy display metadata. +Each present compatibility field produces its own `SQC107`, independently of +whether any other optional field, including `Context Tier`, is present. Missing behavioral fields mean no charter preference. `auto` preserves the author's request for runtime selection and MUST NOT become a literal runtime @@ -232,7 +239,7 @@ Diagnostics sort by line, column, then the code order below. | `SQC102` | warning | Missing canonical Identity ID | | `SQC103` | warning | Missing canonical Identity Purpose | | `SQC104` | warning | Missing canonical responsibility section | -| `SQC105` | warning | Legacy responsibility headings | +| `SQC105` | warning | Legacy responsibility heading; one diagnostic per heading | | `SQC106` | warning | Noncanonical extension heading | | `SQC107` | warning | Compatibility field present | | `SQC108` | warning | Unknown or noncanonically ordered standard section | @@ -247,6 +254,7 @@ Implementations claim operations individually: | `squad-charter/v0.1/validate` | Emit the diagnostics and classification above | | `squad-charter/v0.1/edit` | Canonical serialization plus lossless extension preservation | | `squad-charter/v0.1/legacy-consume` | Accept documented legacy aliases without calling them canonical | +| `squad-charter/v0.1/runtime` | Validate before applying portable behavioral fields, reject invalid input, and expose normalized runtime results | An implementation claiming a capability MUST pass every manifest case that lists that capability. A validation result reports only the validator @@ -260,11 +268,16 @@ enforce tool, filesystem, network, model, and review policy. It MUST NOT grant capabilities because a charter requests them. Links, commands, encoded text, HTML comments, code blocks, and extension content are untrusted data. -A runtime that applies `Preferred`, `Reasoning Effort`, or `Context Tier` MUST -validate the charter first and MUST reject invalid input without applying any -of those fields. The TypeScript compiler validates before resolving behavior. -Its compatibility path for omitted `charterContent` generates a canonical, -behavior-neutral charter; explicitly empty or invalid content is rejected. +A runtime claiming `squad-charter/v0.1/runtime` that applies `Preferred`, +`Reasoning Effort`, or `Context Tier` MUST validate the charter first and MUST +reject invalid input without applying any of those fields. Runtime expectations +in the manifest apply only to cases that declare this capability. + +The TypeScript reference compiler validates before resolving behavior. Its +default profile and its compatibility path that generates a canonical, +behavior-neutral charter when `charterContent` is omitted are convenience +behaviors of that implementation, not requirements for generic validators or +other runtimes. Explicitly empty or invalid content is rejected. ## 12. Mandatory conformance suite @@ -278,6 +291,26 @@ ingestion, and `auto` runtime semantics. The manifest, not the TypeScript harness, defines case inputs, profile selection, paths, edits, outputs, classifications, parsed semantics, and ordered diagnostic ranges. +The language-neutral field contract is +[`manifest.schema.json`](../../test-fixtures/spec/charter-v0.1/manifest.schema.json). +Every manifest contains: + +- `$schema`, `schemaVersion`, `profile`, the complete `capabilities` list, and + uniquely identified `cases`; +- per-case `capabilities`, exactly one input source (`fixture` or `text`), + nullable path/profile inputs, and the named validation API; +- an always-present validation expectation with classification, capability + report, and ordered diagnostic ranges; +- parsed semantics only for `parse` cases, input/edit/output data only for + `edit` cases, and compile/rejection results only for `runtime` cases. + +For a failing runtime case, `errorCodes` is the exact ordered validation-code +sequence returned with the rejection. For a successful runtime case, the three +`resolved*` values are the normalized behavior inputs, where `null` means no +override. Harnesses MUST reject unknown capabilities, missing declared +capability coverage, missing diagnostic-code coverage, and disagreement between +an operation's capability declaration and its expectation fields. + ## 13. Informative TypeScript implementation details The exported TypeScript names, module paths, and example below are informative. @@ -309,6 +342,8 @@ console.log(CHARTER_CAPABILITIES.edit, updated); ## 14. Errata -No errata are recorded for source revision `squad-charter-v0.1-wd3`. Errata for -a future tagged publication will be listed in this section with publication -date, affected section, and correction. +No published revision exists, so there are no publication errata. Working-draft +corrections are recorded as new immutable commits in +[PR #2074](https://github.com/bradygaster/squad/pull/2074). Errata for a future +tagged publication will list its publication date, affected section, and +correction here. diff --git a/docs/specification/core-v0.1.md b/docs/specification/core-v0.1.md index 8c19a57de..ffe5bd488 100644 --- a/docs/specification/core-v0.1.md +++ b/docs/specification/core-v0.1.md @@ -8,12 +8,15 @@ **Editor:** Squad project maintainers -**Publication date:** 2026-09-24 +**Draft date:** 2026-09-24 -**Source revision:** `squad-core-v0.1-wd3` +**Working-draft provenance:** [PR #2074](https://github.com/bradygaster/squad/pull/2074), +whose revision lineage starts at immutable review baseline +[`ab80da18087b7d37755db2c3cc5880d194a10f52`](https://github.com/bradygaster/squad/commit/ab80da18087b7d37755db2c3cc5880d194a10f52). +Each subsequent draft snapshot is identified by its immutable Git commit in +that pull request. -**Immutable publication tag:** Not assigned; this is a source-controlled -working draft +**Immutable publication tag:** None; this working draft is not a publication **Feedback:** [Issue #2069](https://github.com/bradygaster/squad/issues/2069) @@ -157,15 +160,18 @@ backward-compatible additions increment `MINOR`; changes that can alter accepted documents or existing semantics increment `MAJOR` and reset `MINOR` to `0`. -Working-draft source revisions such as `squad-core-v0.1-wd3` identify review -snapshots but are not immutable publications. Implementations MUST NOT infer -compatibility with an unsupported identifier or source revision. +Working-draft snapshots are identified by immutable Git commit SHA and are not +publications. Human-readable draft labels are not revision identifiers. +Implementations MUST NOT infer compatibility with an unsupported profile +identifier or a working-draft commit. ### 10.1 Errata -No errata are recorded for source revision `squad-core-v0.1-wd3`. Errata for a -future tagged publication will be listed in this subsection with publication -date, affected section, and correction. +No published revision exists, so there are no publication errata. Working-draft +corrections are recorded as new immutable commits in +[PR #2074](https://github.com/bradygaster/squad/pull/2074). Errata for a future +tagged publication will list its publication date, affected section, and +correction here. ## 11. Trust boundary diff --git a/packages/squad-sdk/CHANGELOG.md b/packages/squad-sdk/CHANGELOG.md index 3b93da438..74809c1bc 100644 --- a/packages/squad-sdk/CHANGELOG.md +++ b/packages/squad-sdk/CHANGELOG.md @@ -10,7 +10,9 @@ parser surface now includes typed diagnostics, profile and capability metadata, canonical/legacy conformance classification, path-aware identity checks, explicit conformance profile selection, validation-before-compilation, - canonical serialization order, and lossless unknown extension preservation. + a separately declared runtime capability, canonical serialization order, + lossless unknown extension preservation, and a schema-defined portable + manifest with complete diagnostic coverage. ## 0.10.0 diff --git a/packages/squad-sdk/src/agents/charter-compiler.ts b/packages/squad-sdk/src/agents/charter-compiler.ts index 57ab0b8d9..34322fbcc 100644 --- a/packages/squad-sdk/src/agents/charter-compiler.ts +++ b/packages/squad-sdk/src/agents/charter-compiler.ts @@ -42,8 +42,9 @@ export interface CharterCompileOptions { charterContent?: string; /** * Charter profile used for validation. The convenience compiler defaults to - * `squad-charter/v0.1`; portable conformance claims must use the explicit - * conformance validator instead. + * `squad-charter/v0.1`. This TypeScript-specific default is not part of the + * portable runtime capability; portable conformance cases select a profile + * explicitly. */ profile?: string; /** Content of team.md (team roster) */ @@ -166,6 +167,9 @@ export function compileCharter(options: CharterCompileOptions): SquadCustomAgent /** * Compile a charter with full metadata including resolved model/tools. + * + * This is the TypeScript reference runtime adapter. Its omitted-content and + * default-profile conveniences are not requirements for other implementations. * * @param options - Charter compilation options * @returns CompiledCharter with full metadata diff --git a/packages/squad-sdk/src/agents/charter-validator.ts b/packages/squad-sdk/src/agents/charter-validator.ts index 0ecbfd876..0f390dc5d 100644 --- a/packages/squad-sdk/src/agents/charter-validator.ts +++ b/packages/squad-sdk/src/agents/charter-validator.ts @@ -19,6 +19,7 @@ export const CHARTER_CAPABILITIES = { validate: 'squad-charter/v0.1/validate', edit: 'squad-charter/v0.1/edit', legacyConsume: 'squad-charter/v0.1/legacy-consume', + runtime: 'squad-charter/v0.1/runtime', } as const; export type CharterCapability = @@ -351,15 +352,15 @@ function validateCharter( } } - const firstLegacy = scan.sections.find(heading => + const legacySections = scan.sections.filter(heading => LEGACY_RESPONSIBILITY_SECTIONS.some(section => equalName(section, heading.name)), ); - if (firstLegacy) { + for (const legacySection of legacySections) { add( 'SQC105', 'warning', - 'legacy responsibility headings are consumer-compatible but are not canonical v0.1', - headingLocation(firstLegacy), + `legacy responsibility heading "${legacySection.name}" is consumer-compatible but is not canonical v0.1`, + headingLocation(legacySection), ); } @@ -434,20 +435,20 @@ function validateCharter( fieldLocation(contextTier), ); } + } - for (const compatibilityField of COMPATIBILITY_FIELDS) { - for (const field of findFields( - scan.fields, - compatibilityField.name, - compatibilityField.section, - )) { - add( - 'SQC107', - 'warning', - `compatibility field "${compatibilityField.name}" is accepted but is not canonical v0.1`, - fieldLocation(field), - ); - } + for (const compatibilityField of COMPATIBILITY_FIELDS) { + for (const field of findFields( + scan.fields, + compatibilityField.name, + compatibilityField.section, + )) { + add( + 'SQC107', + 'warning', + `compatibility field "${compatibilityField.name}" is accepted but is not canonical v0.1`, + fieldLocation(field), + ); } } diff --git a/test-fixtures/spec/charter-v0.1/manifest.json b/test-fixtures/spec/charter-v0.1/manifest.json index 39a3aec61..3ef4809f3 100644 --- a/test-fixtures/spec/charter-v0.1/manifest.json +++ b/test-fixtures/spec/charter-v0.1/manifest.json @@ -1,11 +1,13 @@ { + "$schema": "./manifest.schema.json", "schemaVersion": 1, "profile": "squad-charter/v0.1", "capabilities": [ "squad-charter/v0.1/parse", "squad-charter/v0.1/validate", "squad-charter/v0.1/edit", - "squad-charter/v0.1/legacy-consume" + "squad-charter/v0.1/legacy-consume", + "squad-charter/v0.1/runtime" ], "cases": [ { @@ -123,7 +125,10 @@ }, { "id": "invalid-behavior-is-rejected-before-runtime-application", - "capabilities": ["squad-charter/v0.1/validate"], + "capabilities": [ + "squad-charter/v0.1/validate", + "squad-charter/v0.1/runtime" + ], "input": { "fixture": "negative/invalid-behavior.md", "text": null, @@ -217,7 +222,9 @@ {"code": "SQC107", "severity": "warning", "start": {"line": 5, "column": 3}, "end": {"line": 5, "column": 11}, "path": "agents/legacy-agent/charter.md"}, {"code": "SQC107", "severity": "warning", "start": {"line": 6, "column": 3}, "end": {"line": 6, "column": 16}, "path": "agents/legacy-agent/charter.md"}, {"code": "SQC107", "severity": "warning", "start": {"line": 7, "column": 3}, "end": {"line": 7, "column": 12}, "path": "agents/legacy-agent/charter.md"}, - {"code": "SQC105", "severity": "warning", "start": {"line": 9, "column": 1}, "end": {"line": 9, "column": 13}, "path": "agents/legacy-agent/charter.md"} + {"code": "SQC105", "severity": "warning", "start": {"line": 9, "column": 1}, "end": {"line": 9, "column": 13}, "path": "agents/legacy-agent/charter.md"}, + {"code": "SQC105", "severity": "warning", "start": {"line": 12, "column": 1}, "end": {"line": 12, "column": 13}, "path": "agents/legacy-agent/charter.md"}, + {"code": "SQC105", "severity": "warning", "start": {"line": 15, "column": 1}, "end": {"line": 15, "column": 16}, "path": "agents/legacy-agent/charter.md"} ] }, "parsed": { @@ -458,7 +465,8 @@ "id": "runtime-auto-remains-selection-not-literal-override", "capabilities": [ "squad-charter/v0.1/parse", - "squad-charter/v0.1/validate" + "squad-charter/v0.1/validate", + "squad-charter/v0.1/runtime" ], "input": { "fixture": null, @@ -759,7 +767,9 @@ {"code": "SQC011", "severity": "error", "start": {"line": 13, "column": 1}, "end": {"line": 13, "column": 13}, "path": "agents/duplicate-agent/charter.md"}, {"code": "SQC105", "severity": "warning", "start": {"line": 13, "column": 1}, "end": {"line": 13, "column": 13}, "path": "agents/duplicate-agent/charter.md"}, {"code": "SQC011", "severity": "error", "start": {"line": 19, "column": 1}, "end": {"line": 19, "column": 13}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC105", "severity": "warning", "start": {"line": 19, "column": 1}, "end": {"line": 19, "column": 13}, "path": "agents/duplicate-agent/charter.md"}, {"code": "SQC011", "severity": "error", "start": {"line": 25, "column": 1}, "end": {"line": 25, "column": 16}, "path": "agents/duplicate-agent/charter.md"}, + {"code": "SQC105", "severity": "warning", "start": {"line": 25, "column": 1}, "end": {"line": 25, "column": 16}, "path": "agents/duplicate-agent/charter.md"}, {"code": "SQC011", "severity": "error", "start": {"line": 31, "column": 1}, "end": {"line": 31, "column": 8}, "path": "agents/duplicate-agent/charter.md"}, {"code": "SQC004", "severity": "error", "start": {"line": 32, "column": 3}, "end": {"line": 32, "column": 16}, "path": "agents/duplicate-agent/charter.md"} ] @@ -877,6 +887,417 @@ "serialization": null, "runtime": null } + }, + { + "id": "empty-charter", + "capabilities": [ + "squad-charter/v0.1/validate" + ], + "input": { + "fixture": null, + "text": "", + "path": null, + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": null, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": false, + "conforms": false, + "classification": "invalid", + "capabilities": [ + "squad-charter/v0.1/validate" + ], + "diagnostics": [ + { + "code": "SQC001", + "severity": "error", + "start": { + "line": 1, + "column": 1 + }, + "end": { + "line": 1, + "column": 1 + }, + "path": null + } + ] + }, + "parsed": null, + "serialization": null, + "runtime": null + } + }, + { + "id": "missing-structural-h1", + "capabilities": [ + "squad-charter/v0.1/validate" + ], + "input": { + "fixture": null, + "text": "## Identity\n- **ID:** `missing-h1`\n- **Purpose:** Exercise the missing H1 diagnostic.\n\n## Accountable Responsibilities\n- Validate portable artifacts.\n\n## Non-Responsibilities\n- Do not infer missing behavior.\n\n## Collaboration and Review Authority\n- Report deterministic results.\n", + "path": "agents/missing-h1/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": null, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": false, + "conforms": false, + "classification": "invalid", + "capabilities": [ + "squad-charter/v0.1/validate" + ], + "diagnostics": [ + { + "code": "SQC002", + "severity": "error", + "start": { + "line": 1, + "column": 1 + }, + "end": { + "line": 1, + "column": 1 + }, + "path": "agents/missing-h1/charter.md" + } + ] + }, + "parsed": null, + "serialization": null, + "runtime": null + } + }, + { + "id": "empty-model-preference", + "capabilities": [ + "squad-charter/v0.1/validate" + ], + "input": { + "fixture": null, + "text": "# Empty Preference — Tester\n\n## Identity\n- **ID:** `empty-preference`\n- **Purpose:** Exercise an empty preferred model.\n\n## Accountable Responsibilities\n- Validate portable artifacts.\n\n## Non-Responsibilities\n- Do not infer missing behavior.\n\n## Collaboration and Review Authority\n- Report deterministic results.\n\n## Model\n- **Preferred:**\n", + "path": "agents/empty-preference/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": null, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": false, + "conforms": false, + "classification": "invalid", + "capabilities": [ + "squad-charter/v0.1/validate" + ], + "diagnostics": [ + { + "code": "SQC006", + "severity": "error", + "start": { + "line": 17, + "column": 3 + }, + "end": { + "line": 17, + "column": 16 + }, + "path": "agents/empty-preference/charter.md" + } + ] + }, + "parsed": null, + "serialization": null, + "runtime": null + } + }, + { + "id": "empty-identity-purpose", + "capabilities": [ + "squad-charter/v0.1/validate" + ], + "input": { + "fixture": null, + "text": "# Empty Purpose — Tester\n\n## Identity\n- **ID:** `empty-purpose`\n- **Purpose:**\n\n## Accountable Responsibilities\n- Validate portable artifacts.\n\n## Non-Responsibilities\n- Do not infer missing behavior.\n\n## Collaboration and Review Authority\n- Report deterministic results.\n", + "path": "agents/empty-purpose/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": null, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": false, + "conforms": false, + "classification": "invalid", + "capabilities": [ + "squad-charter/v0.1/validate" + ], + "diagnostics": [ + { + "code": "SQC009", + "severity": "error", + "start": { + "line": 5, + "column": 3 + }, + "end": { + "line": 5, + "column": 14 + }, + "path": "agents/empty-purpose/charter.md" + } + ] + }, + "parsed": null, + "serialization": null, + "runtime": null + } + }, + { + "id": "missing-identity-section-and-fields", + "capabilities": [ + "squad-charter/v0.1/validate" + ], + "input": { + "fixture": null, + "text": "# Missing Identity — Tester\n\n## Accountable Responsibilities\n- Validate portable artifacts.\n\n## Non-Responsibilities\n- Do not infer missing behavior.\n\n## Collaboration and Review Authority\n- Report deterministic results.\n", + "path": "agents/missing-identity/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": null, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": true, + "conforms": false, + "classification": "noncanonical-compatible", + "capabilities": [ + "squad-charter/v0.1/validate" + ], + "diagnostics": [ + { + "code": "SQC101", + "severity": "warning", + "start": { + "line": 1, + "column": 1 + }, + "end": { + "line": 1, + "column": 1 + }, + "path": "agents/missing-identity/charter.md" + }, + { + "code": "SQC102", + "severity": "warning", + "start": { + "line": 1, + "column": 1 + }, + "end": { + "line": 1, + "column": 1 + }, + "path": "agents/missing-identity/charter.md" + }, + { + "code": "SQC103", + "severity": "warning", + "start": { + "line": 1, + "column": 1 + }, + "end": { + "line": 1, + "column": 1 + }, + "path": "agents/missing-identity/charter.md" + } + ] + }, + "parsed": null, + "serialization": null, + "runtime": null + } + }, + { + "id": "unknown-standard-section", + "capabilities": [ + "squad-charter/v0.1/validate" + ], + "input": { + "fixture": null, + "text": "# Unknown Section — Tester\n\n## Identity\n- **ID:** `unknown-section`\n- **Purpose:** Exercise the unknown-section diagnostic.\n\n## Accountable Responsibilities\n- Validate portable artifacts.\n\n## Non-Responsibilities\n- Do not infer missing behavior.\n\n## Collaboration and Review Authority\n- Report deterministic results.\n\n## Notes\n- This is not a namespaced extension.\n", + "path": "agents/unknown-section/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": null, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": true, + "conforms": false, + "classification": "noncanonical-compatible", + "capabilities": [ + "squad-charter/v0.1/validate" + ], + "diagnostics": [ + { + "code": "SQC108", + "severity": "warning", + "start": { + "line": 16, + "column": 1 + }, + "end": { + "line": 16, + "column": 8 + }, + "path": "agents/unknown-section/charter.md" + } + ] + }, + "parsed": null, + "serialization": null, + "runtime": null + } + }, + { + "id": "compatibility-identity-fields-without-context-tier", + "capabilities": [ + "squad-charter/v0.1/validate", + "squad-charter/v0.1/legacy-consume" + ], + "input": { + "fixture": null, + "text": "# Compatibility Identity — Tester\n\n## Identity\n- **ID:** `compatibility-identity`\n- **Purpose:** Prove compatibility fields are diagnosed independently.\n- **Name:** Compatibility Identity\n- **Role:** Tester\n- **Expertise:** validation, portability\n- **Style:** Deterministic\n\n## Accountable Responsibilities\n- Validate portable artifacts.\n\n## Non-Responsibilities\n- Do not infer missing behavior.\n\n## Collaboration and Review Authority\n- Report deterministic results.\n", + "path": "agents/compatibility-identity/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": null, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": true, + "conforms": false, + "classification": "noncanonical-compatible", + "capabilities": [ + "squad-charter/v0.1/validate" + ], + "diagnostics": [ + { + "code": "SQC107", + "severity": "warning", + "start": { + "line": 6, + "column": 3 + }, + "end": { + "line": 6, + "column": 11 + }, + "path": "agents/compatibility-identity/charter.md" + }, + { + "code": "SQC107", + "severity": "warning", + "start": { + "line": 7, + "column": 3 + }, + "end": { + "line": 7, + "column": 11 + }, + "path": "agents/compatibility-identity/charter.md" + }, + { + "code": "SQC107", + "severity": "warning", + "start": { + "line": 8, + "column": 3 + }, + "end": { + "line": 8, + "column": 16 + }, + "path": "agents/compatibility-identity/charter.md" + }, + { + "code": "SQC107", + "severity": "warning", + "start": { + "line": 9, + "column": 3 + }, + "end": { + "line": 9, + "column": 12 + }, + "path": "agents/compatibility-identity/charter.md" + } + ] + }, + "parsed": null, + "serialization": null, + "runtime": null + } + }, + { + "id": "compatibility-model-fallback-without-context-tier", + "capabilities": [ + "squad-charter/v0.1/validate", + "squad-charter/v0.1/legacy-consume" + ], + "input": { + "fixture": null, + "text": "# Compatibility Fallback — Tester\n\n## Identity\n- **ID:** `compatibility-fallback`\n- **Purpose:** Prove Fallback is diagnosed independently.\n\n## Accountable Responsibilities\n- Validate portable artifacts.\n\n## Non-Responsibilities\n- Do not infer missing behavior.\n\n## Collaboration and Review Authority\n- Report deterministic results.\n\n## Model\n- **Preferred:** auto\n- **Fallback:** example-model\n", + "path": "agents/compatibility-fallback/charter.md", + "profile": "squad-charter/v0.1", + "validationApi": "conformance" + }, + "serialization": null, + "expected": { + "validation": { + "profile": "squad-charter/v0.1", + "accepted": true, + "conforms": false, + "classification": "noncanonical-compatible", + "capabilities": [ + "squad-charter/v0.1/validate" + ], + "diagnostics": [ + { + "code": "SQC107", + "severity": "warning", + "start": { + "line": 18, + "column": 3 + }, + "end": { + "line": 18, + "column": 15 + }, + "path": "agents/compatibility-fallback/charter.md" + } + ] + }, + "parsed": null, + "serialization": null, + "runtime": null + } } ] } diff --git a/test-fixtures/spec/charter-v0.1/manifest.schema.json b/test-fixtures/spec/charter-v0.1/manifest.schema.json new file mode 100644 index 000000000..ddc321f06 --- /dev/null +++ b/test-fixtures/spec/charter-v0.1/manifest.schema.json @@ -0,0 +1,558 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Squad Charter Profile v0.1 conformance manifest", + "type": "object", + "additionalProperties": false, + "required": ["$schema", "schemaVersion", "profile", "capabilities", "cases"], + "properties": { + "$schema": { + "const": "./manifest.schema.json" + }, + "schemaVersion": { + "const": 1 + }, + "profile": { + "const": "squad-charter/v0.1" + }, + "capabilities": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/capability" + } + }, + "cases": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/case" + } + } + }, + "$defs": { + "capability": { + "enum": [ + "squad-charter/v0.1/parse", + "squad-charter/v0.1/validate", + "squad-charter/v0.1/edit", + "squad-charter/v0.1/legacy-consume", + "squad-charter/v0.1/runtime" + ] + }, + "position": { + "type": "object", + "additionalProperties": false, + "required": ["line", "column"], + "properties": { + "line": { + "type": "integer", + "minimum": 1 + }, + "column": { + "type": "integer", + "minimum": 1 + } + } + }, + "diagnostic": { + "type": "object", + "additionalProperties": false, + "required": ["code", "severity", "start", "end", "path"], + "properties": { + "code": { + "enum": [ + "SQC001", + "SQC002", + "SQC003", + "SQC004", + "SQC005", + "SQC006", + "SQC007", + "SQC008", + "SQC009", + "SQC010", + "SQC011", + "SQC012", + "SQC013", + "SQC014", + "SQC015", + "SQC101", + "SQC102", + "SQC103", + "SQC104", + "SQC105", + "SQC106", + "SQC107", + "SQC108" + ] + }, + "severity": { + "enum": ["error", "warning"] + }, + "start": { + "$ref": "#/$defs/position" + }, + "end": { + "$ref": "#/$defs/position" + }, + "path": { + "type": ["string", "null"] + } + } + }, + "validation": { + "type": "object", + "additionalProperties": false, + "required": [ + "profile", + "accepted", + "conforms", + "classification", + "capabilities", + "diagnostics" + ], + "properties": { + "profile": { + "type": "string" + }, + "accepted": { + "type": "boolean" + }, + "conforms": { + "type": "boolean" + }, + "classification": { + "enum": ["canonical", "noncanonical-compatible", "invalid"] + }, + "capabilities": { + "type": "array", + "oneOf": [ + { + "const": [] + }, + { + "const": ["squad-charter/v0.1/validate"] + } + ] + }, + "diagnostics": { + "type": "array", + "items": { + "$ref": "#/$defs/diagnostic" + } + } + } + }, + "parsed": { + "type": "object", + "additionalProperties": false, + "required": [ + "identity", + "ownership", + "boundaries", + "collaboration", + "modelPreference", + "modelRationale", + "modelFallback", + "reasoningEffort", + "authoredReasoningEffort", + "contextTier", + "authoredContextTier" + ], + "properties": { + "identity": { + "type": "object", + "additionalProperties": false, + "required": ["id", "purpose", "name", "role", "expertise", "style"], + "properties": { + "id": { + "type": ["string", "null"] + }, + "purpose": { + "type": ["string", "null"] + }, + "name": { + "type": ["string", "null"] + }, + "role": { + "type": ["string", "null"] + }, + "expertise": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "style": { + "type": ["string", "null"] + } + } + }, + "ownership": { + "type": ["string", "null"] + }, + "boundaries": { + "type": ["string", "null"] + }, + "collaboration": { + "type": ["string", "null"] + }, + "modelPreference": { + "type": ["string", "null"] + }, + "modelRationale": { + "type": ["string", "null"] + }, + "modelFallback": { + "type": ["string", "null"] + }, + "reasoningEffort": { + "type": ["string", "null"] + }, + "authoredReasoningEffort": { + "type": ["string", "null"] + }, + "contextTier": { + "type": ["string", "null"] + }, + "authoredContextTier": { + "type": ["string", "null"] + } + } + }, + "serialization": { + "type": "object", + "additionalProperties": false, + "required": ["mode", "edits"], + "properties": { + "mode": { + "enum": ["canonical", "legacy", "preserve"] + }, + "edits": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["field", "value"], + "properties": { + "field": { + "const": "identity.purpose" + }, + "value": { + "type": "string" + } + } + } + } + } + }, + "serializationExpectation": { + "type": "object", + "additionalProperties": false, + "required": ["output", "outputFixture", "eol", "validation"], + "properties": { + "output": { + "type": ["string", "null"] + }, + "outputFixture": { + "type": ["string", "null"] + }, + "eol": { + "enum": ["LF", "CRLF", "CR"] + }, + "validation": { + "type": "object", + "additionalProperties": false, + "required": ["accepted", "conforms", "classification", "diagnostics"], + "properties": { + "accepted": { + "type": "boolean" + }, + "conforms": { + "type": "boolean" + }, + "classification": { + "enum": ["canonical", "noncanonical-compatible", "invalid"] + }, + "diagnostics": { + "type": "array", + "items": { + "$ref": "#/$defs/diagnostic" + } + } + } + } + } + }, + "runtimeExpectation": { + "type": "object", + "additionalProperties": false, + "required": [ + "compile", + "errorCodes", + "resolvedModel", + "resolvedReasoningEffort", + "resolvedContextTier" + ], + "properties": { + "compile": { + "type": "boolean" + }, + "errorCodes": { + "type": "array", + "items": { + "$ref": "#/$defs/diagnostic/properties/code" + } + }, + "resolvedModel": { + "type": ["string", "null"] + }, + "resolvedReasoningEffort": { + "type": ["string", "null"] + }, + "resolvedContextTier": { + "type": ["string", "null"] + } + } + }, + "case": { + "type": "object", + "additionalProperties": false, + "required": ["id", "capabilities", "input", "serialization", "expected"], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9.-]*$" + }, + "capabilities": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "contains": { + "const": "squad-charter/v0.1/validate" + }, + "items": { + "$ref": "#/$defs/capability" + } + }, + "input": { + "type": "object", + "additionalProperties": false, + "required": ["fixture", "text", "path", "profile", "validationApi"], + "properties": { + "fixture": { + "type": ["string", "null"] + }, + "text": { + "type": ["string", "null"] + }, + "path": { + "type": ["string", "null"] + }, + "profile": { + "type": ["string", "null"] + }, + "validationApi": { + "enum": ["conformance", "convenience"] + } + }, + "oneOf": [ + { + "properties": { + "fixture": { + "type": "string" + }, + "text": { + "type": "null" + } + } + }, + { + "properties": { + "fixture": { + "type": "null" + }, + "text": { + "type": "string" + } + } + } + ] + }, + "serialization": { + "oneOf": [ + { + "$ref": "#/$defs/serialization" + }, + { + "type": "null" + } + ] + }, + "expected": { + "type": "object", + "additionalProperties": false, + "required": ["validation", "parsed", "serialization", "runtime"], + "properties": { + "validation": { + "$ref": "#/$defs/validation" + }, + "parsed": { + "oneOf": [ + { + "$ref": "#/$defs/parsed" + }, + { + "type": "null" + } + ] + }, + "serialization": { + "oneOf": [ + { + "$ref": "#/$defs/serializationExpectation" + }, + { + "type": "null" + } + ] + }, + "runtime": { + "oneOf": [ + { + "$ref": "#/$defs/runtimeExpectation" + }, + { + "type": "null" + } + ] + } + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "capabilities": { + "type": "array", + "contains": { + "const": "squad-charter/v0.1/parse" + } + } + } + }, + "then": { + "properties": { + "expected": { + "type": "object", + "properties": { + "parsed": { + "$ref": "#/$defs/parsed" + } + } + } + } + }, + "else": { + "properties": { + "expected": { + "type": "object", + "properties": { + "parsed": { + "type": "null" + } + } + } + } + } + }, + { + "if": { + "properties": { + "capabilities": { + "type": "array", + "contains": { + "const": "squad-charter/v0.1/edit" + } + } + } + }, + "then": { + "properties": { + "serialization": { + "$ref": "#/$defs/serialization" + }, + "expected": { + "type": "object", + "properties": { + "serialization": { + "$ref": "#/$defs/serializationExpectation" + } + } + } + } + }, + "else": { + "properties": { + "serialization": { + "type": "null" + }, + "expected": { + "type": "object", + "properties": { + "serialization": { + "type": "null" + } + } + } + } + } + }, + { + "if": { + "properties": { + "capabilities": { + "type": "array", + "contains": { + "const": "squad-charter/v0.1/runtime" + } + } + } + }, + "then": { + "properties": { + "expected": { + "type": "object", + "properties": { + "runtime": { + "$ref": "#/$defs/runtimeExpectation" + } + } + } + } + }, + "else": { + "properties": { + "expected": { + "type": "object", + "properties": { + "runtime": { + "type": "null" + } + } + } + } + } + } + ] + } + } +} diff --git a/test/charter-spec.test.ts b/test/charter-spec.test.ts index 1ce09cc2a..6d7abaebf 100644 --- a/test/charter-spec.test.ts +++ b/test/charter-spec.test.ts @@ -3,6 +3,7 @@ import * as path from 'node:path'; import { describe, expect, it } from 'vitest'; import { CHARTER_CAPABILITIES, + CHARTER_DIAGNOSTIC_CODES, CHARTER_PROFILE, CHARTER_PROFILE_METADATA, compileCharterFull, @@ -96,12 +97,24 @@ interface ManifestCase { } interface FixtureManifest { + $schema: './manifest.schema.json'; schemaVersion: 1; profile: string; capabilities: CharterCapability[]; cases: ManifestCase[]; } +interface ManifestSchema { + $defs: { + capability: { enum: CharterCapability[] }; + diagnostic: { + properties: { + code: { enum: string[] }; + }; + }; + }; +} + const FIXTURE_ROOT = path.resolve(__dirname, '..', 'test-fixtures', 'spec', 'charter-v0.1'); async function loadManifest(): Promise { @@ -110,6 +123,12 @@ async function loadManifest(): Promise { ) as FixtureManifest; } +async function loadManifestSchema(manifest: FixtureManifest): Promise { + return JSON.parse( + await readFile(path.join(FIXTURE_ROOT, manifest.$schema), 'utf8'), + ) as ManifestSchema; +} + async function readCaseSource(testCase: ManifestCase): Promise { if (testCase.input.fixture !== null) { return readFile(path.join(FIXTURE_ROOT, testCase.input.fixture), 'utf8'); @@ -178,12 +197,36 @@ function eolName(value: string): 'LF' | 'CRLF' | 'CR' { return 'LF'; } +function runtimeErrorCodes(error: unknown): string[] { + if (typeof error !== 'object' || error === null || !('context' in error)) return []; + const context = error.context; + if (typeof context !== 'object' || context === null || !('metadata' in context)) return []; + const metadata = context.metadata; + if (typeof metadata !== 'object' || metadata === null || !('diagnostics' in metadata)) { + return []; + } + if (!Array.isArray(metadata.diagnostics)) return []; + return metadata.diagnostics.flatMap(diagnostic => + typeof diagnostic === 'object' + && diagnostic !== null + && 'code' in diagnostic + && typeof diagnostic.code === 'string' + ? [diagnostic.code] + : [], + ); +} + describe('Squad Charter Profile v0.1 portable conformance manifest', () => { it('declares the published profile metadata and every operational capability', async () => { const manifest = await loadManifest(); + const schema = await loadManifestSchema(manifest); + expect(manifest.$schema).toBe('./manifest.schema.json'); expect(manifest.schemaVersion).toBe(1); expect(manifest.profile).toBe(CHARTER_PROFILE); expect(manifest.capabilities).toEqual(Object.values(CHARTER_CAPABILITIES)); + expect(schema.$defs.capability.enum).toEqual(manifest.capabilities); + expect(schema.$defs.diagnostic.properties.code.enum) + .toEqual(CHARTER_DIAGNOSTIC_CODES); expect(CHARTER_PROFILE_METADATA).toEqual({ id: CHARTER_PROFILE, artifactClass: 'required', @@ -191,12 +234,54 @@ describe('Squad Charter Profile v0.1 portable conformance manifest', () => { repositoryRelativePath: '.squad/agents/{id}/charter.md', }); + const caseIds = manifest.cases.map(testCase => testCase.id); + expect(new Set(caseIds).size).toBe(caseIds.length); + for (const capability of manifest.capabilities) { expect( manifest.cases.some(testCase => testCase.capabilities.includes(capability)), capability, ).toBe(true); } + + const coveredDiagnostics = new Set( + manifest.cases.flatMap(testCase => + testCase.expected.validation.diagnostics.map(diagnostic => diagnostic.code), + ), + ); + expect( + [...coveredDiagnostics].sort(), + 'every normative diagnostic code has portable manifest coverage', + ).toEqual([...CHARTER_DIAGNOSTIC_CODES].sort()); + + for (const testCase of manifest.cases) { + expect(testCase.capabilities, `${testCase.id}: known capabilities`).toEqual( + testCase.capabilities.filter(capability => + manifest.capabilities.includes(capability), + ), + ); + expect( + testCase.capabilities.includes(CHARTER_CAPABILITIES.validate), + `${testCase.id}: validation capability`, + ).toBe(true); + expect( + testCase.expected.parsed !== null, + `${testCase.id}: parse applicability`, + ).toBe(testCase.capabilities.includes(CHARTER_CAPABILITIES.parse)); + expect( + testCase.serialization !== null + && testCase.expected.serialization !== null, + `${testCase.id}: edit applicability`, + ).toBe(testCase.capabilities.includes(CHARTER_CAPABILITIES.edit)); + expect( + testCase.expected.runtime !== null, + `${testCase.id}: runtime applicability`, + ).toBe(testCase.capabilities.includes(CHARTER_CAPABILITIES.runtime)); + if (testCase.capabilities.includes(CHARTER_CAPABILITIES.runtime)) { + expect(testCase.input.profile, `${testCase.id}: explicit runtime profile`) + .toBe(manifest.profile); + } + } }); it('executes every normative case without synthesizing cases in TypeScript', async () => { @@ -250,7 +335,12 @@ describe('Squad Charter Profile v0.1 portable conformance manifest', () => { .toEqual(testCase.expected.serialization?.validation); } - if (testCase.expected.runtime !== null) { + if (testCase.capabilities.includes(CHARTER_CAPABILITIES.runtime)) { + const runtimeExpectation = testCase.expected.runtime; + expect(runtimeExpectation, `${testCase.id}: runtime expectation`).not.toBeNull(); + if (runtimeExpectation === null) { + throw new Error(`${testCase.id}: runtime capability requires expected.runtime`); + } const compile = () => compileCharterFull({ agentName: 'manifest-agent', charterPath: testCase.input.path ?? 'agents/manifest-agent/charter.md', @@ -258,17 +348,15 @@ describe('Squad Charter Profile v0.1 portable conformance manifest', () => { ...(testCase.input.profile === null ? {} : { profile: testCase.input.profile }), }); - if (!testCase.expected.runtime.compile) { - let message = ''; + if (!runtimeExpectation.compile) { + let errorCodes: string[] = []; try { compile(); } catch (error) { - message = error instanceof Error ? error.message : String(error); - } - expect(message, `${testCase.id}: runtime rejection`).not.toBe(''); - for (const code of testCase.expected.runtime.errorCodes) { - expect(message, `${testCase.id}: ${code}`).toContain(code); + errorCodes = runtimeErrorCodes(error); } + expect(errorCodes, `${testCase.id}: runtime rejection codes`) + .toEqual(runtimeExpectation.errorCodes); } else { const compiled = compile(); expect({ @@ -276,9 +364,9 @@ describe('Squad Charter Profile v0.1 portable conformance manifest', () => { resolvedReasoningEffort: compiled.resolvedReasoningEffort ?? null, resolvedContextTier: compiled.resolvedContextTier ?? null, }, `${testCase.id}: runtime semantics`).toEqual({ - resolvedModel: testCase.expected.runtime.resolvedModel, - resolvedReasoningEffort: testCase.expected.runtime.resolvedReasoningEffort, - resolvedContextTier: testCase.expected.runtime.resolvedContextTier, + resolvedModel: runtimeExpectation.resolvedModel, + resolvedReasoningEffort: runtimeExpectation.resolvedReasoningEffort, + resolvedContextTier: runtimeExpectation.resolvedContextTier, }); } } From 71bd0b3303dd5385294a68b75d7c682f19a46d22 Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Thu, 24 Sep 2026 16:46:12 -0700 Subject: [PATCH 04/11] fix: enforce charter manifest schema Part of #2069 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Source-Commit: 71634fde5ce288868fa2ff00ddfcb8dc7c58d3ee --- docs/specification/charter-v0.1.md | 9 +- package-lock.json | 85 ++++---- package.json | 1 + test-fixtures/spec/charter-v0.1/manifest.json | 25 --- .../spec/charter-v0.1/manifest.schema.json | 2 +- test/charter-spec.test.ts | 196 +++++++++++++++++- 6 files changed, 237 insertions(+), 81 deletions(-) diff --git a/docs/specification/charter-v0.1.md b/docs/specification/charter-v0.1.md index 2167fd2bd..12be9b028 100644 --- a/docs/specification/charter-v0.1.md +++ b/docs/specification/charter-v0.1.md @@ -277,7 +277,9 @@ The TypeScript reference compiler validates before resolving behavior. Its default profile and its compatibility path that generates a canonical, behavior-neutral charter when `charterContent` is omitted are convenience behaviors of that implementation, not requirements for generic validators or -other runtimes. Explicitly empty or invalid content is rejected. +other runtimes. The convenience validator's default-profile behavior is covered +only by reference-implementation tests and is intentionally absent from the +portable manifest. Explicitly empty or invalid content is rejected. ## 12. Mandatory conformance suite @@ -298,7 +300,7 @@ Every manifest contains: - `$schema`, `schemaVersion`, `profile`, the complete `capabilities` list, and uniquely identified `cases`; - per-case `capabilities`, exactly one input source (`fixture` or `text`), - nullable path/profile inputs, and the named validation API; + nullable path/profile inputs, and the portable conformance API; - an always-present validation expectation with classification, capability report, and ordered diagnostic ranges; - parsed semantics only for `parse` cases, input/edit/output data only for @@ -310,6 +312,9 @@ sequence returned with the rejection. For a successful runtime case, the three override. Harnesses MUST reject unknown capabilities, missing declared capability coverage, missing diagnostic-code coverage, and disagreement between an operation's capability declaration and its expectation fields. +Harnesses MUST validate the manifest against `manifest.schema.json` before +executing cases. A manifest that fails schema validation is not a conformance +suite and MUST NOT be partially executed. ## 13. Informative TypeScript implementation details diff --git a/package-lock.json b/package-lock.json index 7331a020d..cb354160a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@typescript-eslint/eslint-plugin": "^8.70.0", "@typescript-eslint/parser": "^8.69.0", "@vitest/coverage-v8": "^4.1.11", + "ajv": "^8.20.0", "cspell": "^10.3.3", "eslint": "^10.10.0", "eslint-plugin-n": "^18.3.0", @@ -1554,24 +1555,6 @@ } } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { - "version": "8.20.0", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { - "version": "1.0.0", - "license": "MIT" - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3101,14 +3084,15 @@ } }, "node_modules/ajv": { - "version": "6.14.0", - "dev": true, + "version": "8.20.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha1-MEs2Nq3Yi6fZNnYN1Q7OAG3qlfk=", "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -3130,24 +3114,6 @@ } } }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.20.0", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "license": "MIT" - }, "node_modules/ansi-escapes": { "version": "7.3.0", "license": "MIT", @@ -4266,6 +4232,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha1-B+mCx0YmFnqnoklcU4F4ktcTlJI=", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "5.0.1", "dev": true, @@ -4285,6 +4268,13 @@ "node": ">= 4" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", + "dev": true, + "license": "MIT" + }, "node_modules/espree": { "version": "11.2.0", "dev": true, @@ -4507,6 +4497,8 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha1-h0v2nG9ATCtdmcSBNBOZ/VWJJjM=", "dev": true, "license": "MIT" }, @@ -5323,8 +5315,9 @@ } }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, + "version": "1.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha1-rnvLNlard6c7pcSb9lTzjmtoYOI=", "license": "MIT" }, "node_modules/json-schema-typed": { @@ -6992,6 +6985,8 @@ }, "node_modules/punycode": { "version": "2.3.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha1-AnQi4vrsCyXhVJw+G9gwm5EztuU=", "dev": true, "license": "MIT", "engines": { @@ -7879,6 +7874,8 @@ }, "node_modules/uri-js": { "version": "4.4.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha1-mxpSWVIlhZ5V9mnZKPiMbFfyp34=", "dev": true, "license": "BSD-2-Clause", "dependencies": { diff --git a/package.json b/package.json index 5a35caaf1..12523d04e 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@typescript-eslint/eslint-plugin": "^8.70.0", "@typescript-eslint/parser": "^8.69.0", "@vitest/coverage-v8": "^4.1.11", + "ajv": "^8.20.0", "cspell": "^10.3.3", "eslint": "^10.10.0", "eslint-plugin-n": "^18.3.0", diff --git a/test-fixtures/spec/charter-v0.1/manifest.json b/test-fixtures/spec/charter-v0.1/manifest.json index 3ef4809f3..42ab97e0c 100644 --- a/test-fixtures/spec/charter-v0.1/manifest.json +++ b/test-fixtures/spec/charter-v0.1/manifest.json @@ -542,31 +542,6 @@ "runtime": null } }, - { - "id": "convenience-validator-defaults-to-v0.1", - "capabilities": ["squad-charter/v0.1/validate"], - "input": { - "fixture": "positive/canonical.md", - "text": null, - "path": null, - "profile": null, - "validationApi": "convenience" - }, - "serialization": null, - "expected": { - "validation": { - "profile": "squad-charter/v0.1", - "accepted": true, - "conforms": true, - "classification": "canonical", - "capabilities": ["squad-charter/v0.1/validate"], - "diagnostics": [] - }, - "parsed": null, - "serialization": null, - "runtime": null - } - }, { "id": "unsupported-profile-fails-closed", "capabilities": ["squad-charter/v0.1/validate"], diff --git a/test-fixtures/spec/charter-v0.1/manifest.schema.json b/test-fixtures/spec/charter-v0.1/manifest.schema.json index ddc321f06..6a2b9b109 100644 --- a/test-fixtures/spec/charter-v0.1/manifest.schema.json +++ b/test-fixtures/spec/charter-v0.1/manifest.schema.json @@ -360,7 +360,7 @@ "type": ["string", "null"] }, "validationApi": { - "enum": ["conformance", "convenience"] + "const": "conformance" } }, "oneOf": [ diff --git a/test/charter-spec.test.ts b/test/charter-spec.test.ts index 6d7abaebf..7dad53c64 100644 --- a/test/charter-spec.test.ts +++ b/test/charter-spec.test.ts @@ -1,5 +1,10 @@ import { readFile } from 'node:fs/promises'; import * as path from 'node:path'; +import Ajv2020, { + type AnySchema, + type ErrorObject, + type ValidateFunction, +} from 'ajv/dist/2020.js'; import { describe, expect, it } from 'vitest'; import { CHARTER_CAPABILITIES, @@ -16,7 +21,7 @@ import { type ParsedCharter, } from '@bradygaster/squad-sdk/parsers'; -type ValidationApi = 'conformance' | 'convenience'; +type ValidationApi = 'conformance'; type SerializationMode = 'canonical' | 'legacy' | 'preserve'; interface ManifestDiagnostic { @@ -104,7 +109,7 @@ interface FixtureManifest { cases: ManifestCase[]; } -interface ManifestSchema { +interface ManifestSchema extends Record { $defs: { capability: { enum: CharterCapability[] }; diagnostic: { @@ -117,18 +122,43 @@ interface ManifestSchema { const FIXTURE_ROOT = path.resolve(__dirname, '..', 'test-fixtures', 'spec', 'charter-v0.1'); -async function loadManifest(): Promise { +async function loadManifestDocument(): Promise { return JSON.parse( await readFile(path.join(FIXTURE_ROOT, 'manifest.json'), 'utf8'), ) as FixtureManifest; } -async function loadManifestSchema(manifest: FixtureManifest): Promise { +async function loadManifestSchema(): Promise { return JSON.parse( - await readFile(path.join(FIXTURE_ROOT, manifest.$schema), 'utf8'), + await readFile(path.join(FIXTURE_ROOT, 'manifest.schema.json'), 'utf8'), ) as ManifestSchema; } +async function compileManifestValidator(): Promise { + const schema = await loadManifestSchema(); + const ajv = new Ajv2020({ + allErrors: true, + allowUnionTypes: true, + strict: true, + }); + return ajv.compile(schema as AnySchema); +} + +async function loadManifest(): Promise { + const manifest = await loadManifestDocument(); + const validateManifest = await compileManifestValidator(); + if (!validateManifest(manifest)) { + throw new Error( + `Invalid charter conformance manifest:\n${JSON.stringify(validateManifest.errors, null, 2)}`, + ); + } + return manifest; +} + +function schemaErrorKeywords(errors: ErrorObject[] | null | undefined): string[] { + return errors?.map(error => error.keyword) ?? []; +} + async function readCaseSource(testCase: ManifestCase): Promise { if (testCase.input.fixture !== null) { return readFile(path.join(FIXTURE_ROOT, testCase.input.fixture), 'utf8'); @@ -142,9 +172,7 @@ function validateCase(testCase: ManifestCase, markdown: string) { ...(testCase.input.path === null ? {} : { path: testCase.input.path }), ...(testCase.input.profile === null ? {} : { profile: testCase.input.profile }), }; - return testCase.input.validationApi === 'conformance' - ? Reflect.apply(validateCharterConformance, undefined, [markdown, options]) - : validateCharterMarkdown(markdown, options); + return Reflect.apply(validateCharterConformance, undefined, [markdown, options]); } function normalizeDiagnostics( @@ -217,9 +245,141 @@ function runtimeErrorCodes(error: unknown): string[] { } describe('Squad Charter Profile v0.1 portable conformance manifest', () => { + it('validates the published manifest against its Draft 2020-12 schema', async () => { + await expect(loadManifest()).resolves.toMatchObject({ + $schema: './manifest.schema.json', + schemaVersion: 1, + profile: CHARTER_PROFILE, + }); + }); + + it('rejects representative schema mutations', async () => { + const manifest = await loadManifestDocument(); + const validateManifest = await compileManifestValidator(); + const firstCase = manifest.cases[0]; + const parseCase = manifest.cases.find(testCase => + testCase.expected.parsed !== null); + expect(firstCase).toBeDefined(); + expect(parseCase).toBeDefined(); + if (firstCase === undefined || parseCase === undefined) { + throw new Error('manifest requires cases for schema mutation coverage'); + } + + const { schemaVersion: _schemaVersion, ...missingRequired } = manifest; + const invalidManifests: Array<{ + name: string; + document: unknown; + keyword: string; + }> = [ + { + name: 'forbidden top-level property', + document: { ...manifest, unexpected: true }, + keyword: 'additionalProperties', + }, + { + name: 'missing required schema version', + document: missingRequired, + keyword: 'required', + }, + { + name: 'invalid validation result type', + document: { + ...manifest, + cases: [ + { + ...firstCase, + expected: { + ...firstCase.expected, + validation: { + ...firstCase.expected.validation, + accepted: 'true', + }, + }, + }, + ...manifest.cases.slice(1), + ], + }, + keyword: 'type', + }, + { + name: 'invalid expectation enum', + document: { + ...manifest, + cases: [ + { + ...firstCase, + expected: { + ...firstCase.expected, + validation: { + ...firstCase.expected.validation, + classification: 'accepted', + }, + }, + }, + ...manifest.cases.slice(1), + ], + }, + keyword: 'enum', + }, + { + name: 'both input sources supplied', + document: { + ...manifest, + cases: [ + { + ...firstCase, + input: { + ...firstCase.input, + fixture: 'positive/canonical.md', + text: '# duplicate source', + }, + }, + ...manifest.cases.slice(1), + ], + }, + keyword: 'oneOf', + }, + { + name: 'invalid case identifier pattern', + document: { + ...manifest, + cases: [ + { ...firstCase, id: 'INVALID_CASE_ID' }, + ...manifest.cases.slice(1), + ], + }, + keyword: 'pattern', + }, + { + name: 'parsed expectation without parse capability', + document: { + ...manifest, + cases: manifest.cases.map(testCase => + testCase.id === parseCase.id + ? { + ...testCase, + capabilities: testCase.capabilities.filter( + capability => capability !== CHARTER_CAPABILITIES.parse, + ), + } + : testCase), + }, + keyword: 'type', + }, + ]; + + for (const mutation of invalidManifests) { + expect(validateManifest(mutation.document), mutation.name).toBe(false); + expect( + schemaErrorKeywords(validateManifest.errors), + `${mutation.name}: schema keyword`, + ).toContain(mutation.keyword); + } + }); + it('declares the published profile metadata and every operational capability', async () => { const manifest = await loadManifest(); - const schema = await loadManifestSchema(manifest); + const schema = await loadManifestSchema(); expect(manifest.$schema).toBe('./manifest.schema.json'); expect(manifest.schemaVersion).toBe(1); expect(manifest.profile).toBe(CHARTER_PROFILE); @@ -373,3 +533,21 @@ describe('Squad Charter Profile v0.1 portable conformance manifest', () => { } }); }); + +describe('TypeScript charter validator reference behavior', () => { + it('defaults the convenience validator to v0.1 without making it portable', async () => { + const markdown = await readFile( + path.join(FIXTURE_ROOT, 'positive', 'canonical.md'), + 'utf8', + ); + + expect(validateCharterMarkdown(markdown)).toMatchObject({ + profile: CHARTER_PROFILE, + accepted: true, + conforms: true, + conformance: 'canonical', + capabilities: [CHARTER_CAPABILITIES.validate], + diagnostics: [], + }); + }); +}); From f0811502797219d2bae028d45e8d25492cbd9ffc Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Fri, 25 Sep 2026 07:28:50 -0700 Subject: [PATCH 05/11] docs: draft Squad specification suite Part of #2069 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> (cherry picked from commit c03ba950fde071c8ec58a64504fdb7b5cf1206ef) --- docs/specification/README.md | 56 +++++- docs/specification/artifact-inventory.md | 94 ++++++--- docs/specification/automation-v0.1.md | 80 ++++++++ docs/specification/ceremony-v0.1.md | 81 ++++++++ .../configuration-casting-v0.1.md | 100 ++++++++++ .../coordination-handoff-v0.1.md | 79 ++++++++ .../execution-preferences-v0.1.md | 94 +++++++++ .../github-work-lifecycle-binding-v0.1.md | 106 ++++++++++ docs/specification/governance-review-v0.1.md | 117 +++++++++++ .../interoperability-conventions-v0.1.md | 131 ++++++++++++ .../specification/planning-activation-v0.1.md | 100 ++++++++++ .../state-memory-requirements-v0.1.md | 81 ++++++++ docs/specification/team-routing-v0.1.md | 103 ++++++++++ docs/specification/work-lifecycle-v0.1.md | 89 +++++++++ .../spec/suite-v0.1/evidence.schema.json | 97 +++++++++ .../governance-rejection.example.json | 45 +++++ test-fixtures/spec/suite-v0.1/index.json | 188 ++++++++++++++++++ .../spec/suite-v0.1/index.schema.json | 142 +++++++++++++ test/specification-suite.test.ts | 123 ++++++++++++ 19 files changed, 1877 insertions(+), 29 deletions(-) create mode 100644 docs/specification/automation-v0.1.md create mode 100644 docs/specification/ceremony-v0.1.md create mode 100644 docs/specification/configuration-casting-v0.1.md create mode 100644 docs/specification/coordination-handoff-v0.1.md create mode 100644 docs/specification/execution-preferences-v0.1.md create mode 100644 docs/specification/github-work-lifecycle-binding-v0.1.md create mode 100644 docs/specification/governance-review-v0.1.md create mode 100644 docs/specification/interoperability-conventions-v0.1.md create mode 100644 docs/specification/planning-activation-v0.1.md create mode 100644 docs/specification/state-memory-requirements-v0.1.md create mode 100644 docs/specification/team-routing-v0.1.md create mode 100644 docs/specification/work-lifecycle-v0.1.md create mode 100644 test-fixtures/spec/suite-v0.1/evidence.schema.json create mode 100644 test-fixtures/spec/suite-v0.1/governance-rejection.example.json create mode 100644 test-fixtures/spec/suite-v0.1/index.json create mode 100644 test-fixtures/spec/suite-v0.1/index.schema.json create mode 100644 test/specification-suite.test.ts diff --git a/docs/specification/README.md b/docs/specification/README.md index a8ec32404..7d5f3252c 100644 --- a/docs/specification/README.md +++ b/docs/specification/README.md @@ -4,7 +4,7 @@ **Status:** Working Draft -**Draft date:** 2026-09-24 +**Draft date:** 2026-09-25 **Working-draft provenance:** [PR #2074](https://github.com/bradygaster/squad/pull/2074), whose revision lineage starts at immutable review baseline @@ -14,7 +14,7 @@ that pull request; no publication tag has been assigned. **Document class:** Informative index; linked core/profile documents identify their normative sections -**First artifact profile:** [Squad Charter Profile v0.1](charter-v0.1.md) +**Foundation profile:** [Squad Charter Profile v0.1](charter-v0.1.md) This directory defines portable contracts for the on-disk artifacts that make up a Squad. The specification is independent of the TypeScript reference @@ -25,14 +25,62 @@ without claiming that an entire Squad is conformant. - [Core v0.1](core-v0.1.md) defines shared terminology, conformance classes, versions, paths, extensions, and trust requirements. +- [Interoperability Conventions v0.1](interoperability-conventions-v0.1.md) + defines suite discovery, stable authority identities, negotiation, shared + evidence, replay protection, and privacy/secrets exclusion. - [Charter Profile v0.1](charter-v0.1.md) defines the first normative artifact profile for `.squad/agents/{id}/charter.md`. -- [Artifact inventory](artifact-inventory.md) records the current on-disk - surface, parser locations, maturity, and proposed profile order. +- [Team and Routing Profile v0.1](team-routing-v0.1.md) defines accountable + roster entries, capability ownership, and deterministic route selection. +- [Configuration and Casting Profile v0.1](configuration-casting-v0.1.md) + defines configuration precedence, cast provenance, and revision joins. +- [Governance and Review Profile v0.1](governance-review-v0.1.md) defines + reviewer independence, rejection lockout, reassignment, escalation, and + approval evidence. +- [Execution Preferences Profile v0.1](execution-preferences-v0.1.md) separates + portable execution intent from provider catalogs and runtime policy. +- [Planning and Activation Profile v0.1](planning-activation-v0.1.md) defines + PRD intake, decomposition, acceptance, and activation evidence. +- [Ceremony Profile v0.1](ceremony-v0.1.md) defines observable ceremony + declarations and completion records without standardizing meeting culture. +- [Work Lifecycle Profile v0.1](work-lifecycle-v0.1.md) defines the + provider-neutral issue-to-change lifecycle. +- [GitHub Work Lifecycle Binding v0.1](github-work-lifecycle-binding-v0.1.md) + maps the neutral lifecycle to GitHub issues, labels, branches, pull requests, + checks, reviews, merges, and closure. +- [Coordination and Handoff Profile v0.1](coordination-handoff-v0.1.md) defines + portable handoff envelopes and idempotent delivery semantics. +- [Automation Profile v0.1](automation-v0.1.md) defines schedule manifests, + execution claims, retry evidence, and provider boundaries. +- [State and Memory Requirements v0.1](state-memory-requirements-v0.1.md) is an + informative requirements draft with explicit promotion criteria. +- [Artifact inventory and coverage matrix](artifact-inventory.md) traces the + repository surface to its specification disposition and maturity. - [`test-fixtures/spec/charter-v0.1/`](../../test-fixtures/spec/charter-v0.1/) contains language-neutral positive, negative, legacy, and round-trip fixtures. Its [`manifest.schema.json`](../../test-fixtures/spec/charter-v0.1/manifest.schema.json) defines the portable manifest field contract. +- [`test-fixtures/spec/suite-v0.1/`](../../test-fixtures/spec/suite-v0.1/) + contains the `squad-conformance/v0.1` suite index, reusable evidence schema, + and language-neutral governance evidence example. The index registers the + existing Charter v0.1 manifest unchanged as a legacy dialect. + +## Dependency layers + +1. `squad-core/v0.1` defines common conformance, diagnostics, versioning, + extension, and trust rules. +2. `squad-interop/v0.1` defines discovery, negotiation, authority identity, + shared evidence, concurrency, replay protection, and secrets exclusion. +3. Artifact profiles define portable files and evidence independent of any + hosted provider. +4. Protocol profiles define state transitions and messages spanning artifacts. +5. Provider bindings map neutral protocol concepts to provider resources. +6. Informative requirements documents preserve conceptual coverage where + interoperability evidence is not mature enough for a truthful schema. + +Conformance is capability-specific. Supporting a GitHub binding does not imply +support for every artifact profile, and accepting one profile operation does +not imply editor, validator, executor, or provider capability. Normative references are maintained in [Core v0.1 §2](core-v0.1.md#2-normative-references), including BCP 14, diff --git a/docs/specification/artifact-inventory.md b/docs/specification/artifact-inventory.md index 2bcbe77b9..5d3b979b4 100644 --- a/docs/specification/artifact-inventory.md +++ b/docs/specification/artifact-inventory.md @@ -2,28 +2,40 @@ **Document class:** Informative -**Audit date:** 2026-09-24 +**Audit date:** 2026-09-25 -**Purpose:** Profile planning and parser-drift evidence for issue #2069. +**Purpose:** Repository-derived traceability, profile planning, and +interoperability maturity evidence for issue #2069. Paths in this table are Squad-root-relative. Repository-local installations render them beneath `.squad/`. -| Artifact | Artifact class | Current authority/consumer | Maturity | Proposed profile | +| Surface | Repository evidence | Disposition | Specification | Maturity or promotion rationale | |---|---|---|---|---| -| `agents/{id}/charter.md` | required | `packages/squad-sdk/src/agents/charter-compiler.ts` | Draft profile shipped here | Charter v0.1 | -| `team.md` | required | config Markdown migration, presets, coordinator | Multiple table shapes remain | Roster and routing | -| `routing.md` | required | `packages/squad-sdk/src/config/routing.ts` | Parsed but not formally versioned | Roster and routing | -| `config.json` | optional | config schema/loaders | Structured schema exists | Configuration and casting | -| `casting/registry.json` | runtime-state | casting provenance/state APIs | Versioned implementation schema | Configuration and casting | -| `casting/history.json` | runtime-state | casting atomic update path | Backend-sensitive | Configuration and casting | -| `skills/{name}/SKILL.md` | optional | skill loader and coordinator discovery | Frontmatter conventions exist | Skills | -| `decisions.md` | runtime-state | state bridge, coordinator, Scribe | Authority/backend semantics evolving | Decisions and memory | -| `decisions/inbox/**` | runtime-state | governed state bridge | Drop-box protocol, backend-sensitive | Decisions and memory | -| `agents/{id}/history.md` | runtime-state | history/state APIs | Backend-sensitive | Decisions and memory | -| `identity/**` | runtime-state | coordinator/state bridge | Backend-sensitive | Deferred state profile | -| generated capability blocks | generated | `team-capabilities` generator | Reproducible output | Implementation profile | -| `.github/agents/squad.agent.md` | generated/runtime prompt | Copilot surfaces | Product integration artifact | Implementation profile | +| Discovery, negotiation, authority identity, shared evidence, secrets | Core/profile metadata, registry IDs, events, audit records | Stable normative dependency | Interoperability Conventions v0.1 | Cross-cutting mechanics must be shared before profile-specific conformance | +| Agent charter | `agents/{id}/charter.md`, compiler, validator, fixtures | Normative profile now | Charter v0.1 | Stable parser and round-trip evidence | +| Team roster | `team.md`, roster templates, presets | Normative profile now | Team and Routing v0.1 | Stable identity/accountability join; presentation variants remain compatible | +| Routing ownership | `routing.md`, routing parser, coordinator tests | Normative profile now | Team and Routing v0.1 | Deterministic table subset and fallback semantics are observable | +| Runtime configuration | `config.json`, config schema and loaders | Normative profile now | Configuration and Casting v0.1 | Portable intent standardized; implementation-only keys remain extensions | +| Casting provenance | `casting/registry.json`, `policy.json`, `history.json` | Normative profile now | Configuration and Casting v0.1 | Registry identity and revision join are stable; theme selection algorithm is informative | +| Governance and review | `governance.md`, reviewer protocol, PR requirements | Normative protocol now | Governance and Review v0.1 | Lockout, evidence, reassignment, escalation, and approval are externally auditable | +| Execution preferences | charters, config, model selector, lifecycle manager | Normative profile now | Execution Preferences v0.1 | Portable intent is stable; model catalogs and authorization remain runtime policy | +| PRD intake and planning | `prd-intake.md`, planning ontology, gh-aw workflows | Normative protocol now | Planning and Activation v0.1 | Artifact kinds, acceptance gates, and idempotency are observable | +| Ceremonies | `ceremonies.md`, ceremony reference, retro workflow | Normative artifact and record now | Ceremony v0.1 | Inputs, decisions, actions, roles, trigger, and cooldown are portable; meeting style is informative | +| Work lifecycle | issue lifecycle template, platform interfaces, worktree tests | Normative protocol now | Work Lifecycle v0.1 | Neutral states and evidence can be provider-independent | +| GitHub lifecycle | GitHub adapter, gh-aw workflows, issue/PR templates | Provider binding now | GitHub Work Lifecycle Binding v0.1 | GitHub resource mappings and failure semantics are observable | +| Handoffs and sessions | spawn reference, lifecycle manager, event buses | Normative envelope now | Coordination and Handoff v0.1 | Message identity and idempotency are portable; session transport internals are not | +| State backends | state backend interfaces, worktree/orphan/two-layer implementations | Informative requirements now | State and Memory Requirements v0.1 | Authority and recovery vary by backend; promotion requires backend-neutral CAS fixtures | +| Memory governance | memory classes, providers, audit records | Informative requirements now | State and Memory Requirements v0.1 | Classification is promising but provider naming and durable authority are not stable | +| Automation | schedule manifest, scheduler, local and GitHub providers | Normative profile now | Automation v0.1 | Manifest, claims, retry, and result evidence are stable | +| Skills | `skills/{name}/SKILL.md`, loader and discovery | Deferred | Future Skills Profile | Promote after metadata precedence, imports, script trust, and compatibility fixtures stabilize | +| Coordinator prompts and UI wording | templates, generated agent prompt, shell | Informative guidance | Implementation documentation | Prompts and wording are implementation inputs, not interoperability contracts | +| Telemetry and cost accounting | OpenTelemetry and cost modules | Deferred | Future Observability Binding | Promote after common event identity and privacy/redaction contracts stabilize | +| Communications providers | GitHub Discussions, ADO, Teams, file log adapters | Deferred provider bindings | Future Communication Bindings | Common update/reply shape exists; authorization and delivery guarantees differ materially | +| Azure DevOps and Planner lifecycle | platform adapters and issue lifecycle template | Deferred provider bindings | Future ADO and Planner Bindings | Require provider-specific fixtures for hierarchy, review, checks, and closure | +| Plugins, marketplace, MCP, and external tools | marketplace and MCP modules | Deferred extension bindings | Future Extension Discovery Profiles | Discovery, signatures, grants, and trust policy require separate security review | +| Generated capability blocks | team capability generator | Informative guidance | Implementation documentation | Reproducible output is implementation-derived, not authority | +| `.github/agents/squad.agent.md` | generated prompt and workflow installation | Informative guidance | Implementation documentation | Product integration artifact; prompts and internal algorithms remain non-normative | ## Charter conformance audit @@ -45,14 +57,46 @@ deterministic validator plus an independently declared runtime capability. The portable conformance manifest is governed by a language-neutral JSON Schema and covers every normative diagnostic code. -## Profile sequencing +## Promotion roadmap -1. Charter v0.1: identity, responsibility semantics, behavioral preferences, - diagnostics, legacy aliases, and fixtures. -2. Roster and routing: joins among agent ID, directory, roster, and ownership. -3. Configuration and casting: schemas, provenance, revisions, and precedence. -4. Skills: discovery, metadata, trust, and compatibility. -5. Decisions, memory, and mutable state after backend authority stabilizes. +The v0.1 suite establishes clean boundaries rather than claiming every surface +is equally mature. The next promotion sequence is: -Runtime protocols, prompts, CLI UX, telemetry, and hosted publication remain -informative or implementation-specific until a profile explicitly adopts them. +1. Add cross-implementation fixtures for team/config/governance evidence. +2. Prove provider-neutral work lifecycle behavior against GitHub and one + non-GitHub binding. +3. Prove state authority, compare-and-swap, recovery, and migration across + local, orphan-ref, and external backends before promoting state/memory. +4. Standardize skills only after discovery, imports, executable content, and + trust policy have interoperable fixtures. +5. Add communication and observability bindings only when delivery guarantees, + event identity, redaction, and authorization can be stated without encoding + a single product implementation. + +## Known unstable and non-conforming behavior + +**Recorded:** 2026-09-25 + +- Current `.squad/governance.md` permits accountable-owner revision after a + rejection; the reviewer protocol and Governance Profile require independent + revision-author lockout. +- Current model selectors contain vendor names and fallback lists. Those are + implementation policy and do not conform as portable preference vocabulary. +- Current ceremony conditions are natural language. They are compatible input, + not canonical deterministic trigger definitions. +- Current state backends do not share one backend-neutral recovery and + migration fixture set, so state/memory remains informative. +- Current GitHub adapters rely on installed `gh` behavior and do not yet expose + API-version negotiation evidence required by the GitHub binding. +- Casting universe lists and persona names are volatile display metadata and + MUST NOT be used as authority identity. +- Publication requires an independent reviewer for each normative profile and + binding; this working draft supplies reviewable contracts, not publication. +- The reusable conformance dialect still needs shared document-case, + diagnostic, transition-trace, provider-transcript, result, and security-case + schemas before runtime or provider claims can be published. +- Column, tab, Unicode normalization, JSON duplicate-key and number handling, + canonical JSON digests, idempotency retention, snapshot clocks, evidence + ordering, provider transcript sanitization, partial-failure taxonomy, + pass/fail/skip semantics, required extensions, and deprecation windows remain + explicit publication blockers. diff --git a/docs/specification/automation-v0.1.md b/docs/specification/automation-v0.1.md new file mode 100644 index 000000000..f3f069722 --- /dev/null +++ b/docs/specification/automation-v0.1.md @@ -0,0 +1,80 @@ +# Squad Automation Profile v0.1 + +**Status:** Working Draft + +**Document class:** Normative unless marked informative + +**Profile identifier:** `squad-automation/v0.1` + +**Maturity:** Experimental normative + +**Depends on:** `squad-core/v0.1`, `squad-interop/v0.1`, +`squad-coordination-handoff/v0.1` + +**Artifact:** `schedule.json` + +## 1. Scope and non-goals + +This profile defines schedule entries, triggers, execution claims, retries, +results, and provider generation boundaries. It does not standardize cron +engines, host-local time policy, workflow syntax, scripts, prompt content, or +provider deployment mechanisms. + +## 2. Manifest model + +A manifest declares version and schedules. Each schedule declares ID, name, +enabled state, trigger, task reference, provider set, retry policy, and optional +concurrency key. Trigger types are `cron`, `interval`, `event`, and `startup`. +Task classes are `workflow`, `script`, `agent`, and `webhook`; bindings map +class names to implementation operations. + +Task references are inert data. Consumers MUST NOT execute a task until +authorization and provider policy checks succeed. + +## 3. Execution states + +`eligible -> claimed -> running -> succeeded|failed|cancelled|timed-out` + +Retry creates a new attempt under the same run ID. Attempt count, backoff, start, +finish, output reference, error class, and next due time are recorded. +Run and attempt records use the shared evidence envelope. + +## 4. Trigger and time semantics + +Cron bindings MUST declare time zone and daylight-saving behavior. Interval +triggers use elapsed duration, not calendar arithmetic. Event triggers require +a stable event ID. Startup triggers are idempotent per executor startup ID. + +## 5. Capabilities and roles + +Capabilities are `automation.consume`, `automation.evaluate`, +`automation.claim`, `automation.execute`, and `automation.generate-provider`. +Evaluators decide eligibility; executors claim and run; generators emit +provider-native configuration but do not become manifest authority. + +## 6. Failure, retry, and concurrency + +Invalid triggers, unknown providers, denied tasks, claim conflict, timeout, +spawn failure, nonzero result, generation drift, and exhausted retries are +distinct failures. Claims use compare-and-swap. A run key deduplicates retries +and multi-provider delivery. Provider generation MUST be reproducible from the +same manifest and generator version. + +## 7. Versioning and extensions + +Manifest, provider binding, and generated workflow versions are independent. +Unknown namespaced fields round-trip. Unsupported task or trigger types fail +closed. + +## 8. Security and privacy + +Script and webhook execution cross a trust boundary and require explicit +authorization. Arguments MUST remain vectors rather than shell-concatenated +strings. Logs and results MUST redact secrets and SHOULD store large output by +reference. + +## 9. Conformance + +Tests MUST cover every trigger, time zone declaration, duplicate event, +startup deduplication, claim race, retry and backoff, timeout, provider +generation drift, denied execution, and secret-safe diagnostics. diff --git a/docs/specification/ceremony-v0.1.md b/docs/specification/ceremony-v0.1.md new file mode 100644 index 000000000..9a4c6fb77 --- /dev/null +++ b/docs/specification/ceremony-v0.1.md @@ -0,0 +1,81 @@ +# Squad Ceremony Profile v0.1 + +**Status:** Working Draft + +**Document class:** Normative unless marked informative + +**Profile identifier:** `squad-ceremony/v0.1` + +**Maturity:** Experimental normative + +**Depends on:** `squad-core/v0.1`, `squad-interop/v0.1`, +`squad-team-routing/v0.1` + +**Artifact:** `ceremonies.md` or an equivalent manifest selected by an API + +## 1. Scope and non-goals + +This profile defines observable ceremony declarations, triggers, cooldowns, +participants, decisions, actions, and completion records. It does not +standardize meeting culture, agenda prose, facilitation prompts, timekeeping +style, consensus, or synchronous human attendance. + +## 2. Declaration model + +A ceremony declares ID, enabled state, trigger mode, timing, condition +reference, facilitator selector, participant selectors, cooldown policy, input +artifact kinds, and output requirements. Trigger modes are `manual`, `event`, +and `schedule`. Natural-language conditions MAY be preserved but are +noncanonical unless paired with a namespaced deterministic evaluator. + +Participant selectors are `accountable-owner`, `all-involved`, +`all-relevant`, or explicit member IDs. Resolution MUST produce the exact +participant set used. + +## 3. Execution record + +Every run records ceremony ID, run ID, trigger evidence, input revisions, +facilitator, participants, start and completion times, status, decisions, +actions, and next eligible time. Decisions and actions require stable IDs, +owners, and dispositions. + +## 4. State transitions + +`eligible -> claimed -> running -> completed|failed|cancelled` + +Cooldown begins at terminal completion. A run during cooldown is `suppressed` +with evidence and MUST NOT create a second active claim. Failure policy is +`blocking` or `advisory`; advisory failure may permit the surrounding workflow +to continue but MUST remain visible. + +## 5. Capabilities and roles + +Capabilities are `ceremony.consume`, `ceremony.evaluate`, +`ceremony.facilitate`, and `ceremony.record`. The facilitator structures the +run; participants provide inputs; the recorder persists outcomes; the +coordinator enforces claim and cooldown. + +## 6. Canonical and compatible behavior + +Canonical declarations use deterministic trigger references and participant +selectors. Compatible consumers MAY ingest current Markdown tables and free +text conditions but MUST classify evaluator-dependent behavior as informative. + +## 7. Diagnostics, idempotency, and concurrency + +Unknown members, unresolved selectors, overlapping claims, missing required +outputs, invalid cooldown, and stale input revisions are explicit failures. +The tuple `(ceremony ID, trigger event ID, input revision set)` is idempotent. +Claims require compare-and-swap so only one executor runs a ceremony instance. + +## 8. Versioning, extensions, security, and privacy + +Agenda and output extensions use namespaces and round-trip. Ceremony prose is +untrusted and cannot grant tools or authorization. Records SHOULD minimize +personal attendance data and MUST NOT copy secrets from source artifacts. + +## 9. Conformance + +Tests MUST cover manual, event, and schedule triggers; selector resolution; +cooldown suppression; advisory and blocking failures; duplicate claims; stale +inputs; and decision/action output validation. diff --git a/docs/specification/configuration-casting-v0.1.md b/docs/specification/configuration-casting-v0.1.md new file mode 100644 index 000000000..130ab654b --- /dev/null +++ b/docs/specification/configuration-casting-v0.1.md @@ -0,0 +1,100 @@ +# Squad Configuration and Casting Profile v0.1 + +**Status:** Working Draft + +**Document class:** Normative unless marked informative + +**Profile identifier:** `squad-configuration-casting/v0.1` + +**Maturity:** Experimental normative + +**Depends on:** `squad-core/v0.1`, `squad-interop/v0.1`, +`squad-team-routing/v0.1` + +**Artifacts:** `config.json`, `casting/registry.json`, `casting/policy.json`, +`casting/history.json` + +## 1. Scope and non-goals + +This profile defines portable configuration selection, cast identity +provenance, revisions, and roster joins. It does not standardize themed +universes, character-selection scoring, UI choices, provider credentials, or +internal migration algorithms. + +Configuration and provenance are normative. Casting aesthetics are an +informative, independently versioned extension and MUST NOT define authority. + +## 2. Data model and authority + +`config.json` is authored configuration intent. `casting/registry.json` is the +current cast identity authority. `casting/history.json` is append-only +provenance and MUST NOT override the registry. `casting/policy.json` constrains +allowed naming behavior but does not itself create members. + +Every registry document MUST declare a schema identifier, schema version, +monotonic revision, generation timestamp, and agent map keyed by member ID. +Each agent entry MUST include display name, persistent name, accountable role, +universe or naming source, status, creation time, and update time. + +## 3. Precedence and joins + +Portable configuration precedence is: + +1. explicit operation input; +2. session-scoped user intent; +3. repository `config.json`; +4. charter preference; +5. runtime automatic policy. + +Lower layers MUST NOT overwrite higher-layer authored intent. A value of +`auto` requests runtime selection and is not a concrete override. + +Registry IDs MUST join to active team IDs. Recasting MAY change display names +but MUST preserve member IDs unless an explicit identity migration is recorded. + +## 4. State transitions + +`unregistered -> active -> inactive -> retired` is the portable lifecycle. +`retired -> active` requires a new revision and explicit reactivation evidence. +Every mutation increments `revision` exactly once and appends history with the +operation ID, prior revision, resulting revision, actor, and timestamp. + +## 5. Capabilities and roles + +Capabilities are `config.consume`, `config.edit`, `casting.produce`, +`casting.validate`, `casting.recast`, and `casting.migrate`. Producers create +revisions; validators check joins and monotonicity; executors apply an +authorized cast operation. + +## 6. Canonical, compatible, and failure behavior + +Canonical documents use declared schemas and complete provenance. Compatible +consumers MAY ingest older registry shapes but MUST report the migration and +MUST NOT fabricate provenance. Unknown configuration keys are preserved as +namespaced extensions. Invalid joins, revision regression, duplicate persistent +names where uniqueness is required, or stale writes fail closed. + +## 7. Idempotency, retry, and concurrency + +Operations MUST carry an idempotency key. Repeating a completed key returns the +same resulting revision. Concurrent writers MUST compare the expected registry +revision; a mismatch returns `stale-revision` without partial mutation. + +## 8. Versioning and extensions + +Artifact schema versions and this profile version are independent. Consumers +MUST negotiate each supported schema. Namespaced policy and registry fields +MUST round-trip unchanged. + +## 9. Security, privacy, and trust + +Configuration is not authorization. Model names, tools, repositories, and +providers are requests subject to runtime policy. Registry content MUST NOT +contain credentials. Theme or persona text is untrusted and MUST NOT grant +capabilities. + +## 10. Examples and conformance + +The topology example in `test-fixtures/spec/suite-v0.1/` demonstrates roster, +charter, and registry joins. Conformance requires revision, idempotency, +migration, extension-preservation, and stale-write fixtures. diff --git a/docs/specification/coordination-handoff-v0.1.md b/docs/specification/coordination-handoff-v0.1.md new file mode 100644 index 000000000..5f5a613e1 --- /dev/null +++ b/docs/specification/coordination-handoff-v0.1.md @@ -0,0 +1,79 @@ +# Squad Coordination and Handoff Profile v0.1 + +**Status:** Working Draft + +**Document class:** Normative unless marked informative + +**Profile identifier:** `squad-coordination-handoff/v0.1` + +**Maturity:** Experimental normative + +**Depends on:** `squad-core/v0.1`, `squad-interop/v0.1`, +`squad-team-routing/v0.1` + +## 1. Scope and non-goals + +This profile defines portable task dispatch, handoff, result, escalation, and +session-reference envelopes. It does not standardize prompts, tool APIs, +transport, streaming, agent internals, UI notifications, or session storage. + +## 2. Envelope + +Every message uses the shared evidence envelope and declares message type, +message ID, correlation ID, +causation ID, sender, intended recipient or route, task or artifact reference, +input revisions, created time, expiry policy, idempotency key, and payload +digest. Types are `dispatch`, `acknowledgement`, `progress`, `result`, +`handoff`, `escalation`, and `cancellation`. + +Session IDs are opaque provider references. Portable consumers MUST NOT derive +identity, authorization, or ordering from their syntax. + +## 3. State transitions + +`pending -> accepted -> running -> succeeded|failed|cancelled|expired` + +A handoff creates a successor task linked by causation; it does not mutate the +predecessor result. An escalation identifies the blocked capability, evidence, +and requested authority. Result completion requires output references and +their revisions. + +## 4. Delivery, idempotency, and ordering + +Delivery is at-least-once unless a binding declares stronger guarantees. +Consumers deduplicate by message ID and operation idempotency key. Duplicate +delivery returns the original disposition. Per-correlation ordering uses +causation links, not transport arrival time. Concurrent results for one task +require an explicit winner or are a conflict. + +## 5. Capabilities and roles + +Capabilities are `coordination.dispatch`, `coordination.consume`, +`coordination.handoff`, `coordination.cancel`, and `coordination.recover`. +Coordinators route; workers consume and produce results; authorities resolve +escalations; recorders persist evidence. + +## 6. Failure and recovery + +Unknown recipient, expired task, stale input, digest mismatch, duplicate +conflict, unavailable transport, partial result, and lost session are distinct +failures. Recovery MAY create a new provider session but MUST retain the same +portable task identity and record the replacement reference. + +## 7. Versioning and extensions + +Bindings negotiate transports separately. Namespaced payload fields +round-trip. Unknown mandatory message types fail closed. + +## 8. Security and privacy + +Messages are untrusted and do not grant tools, repository access, or provider +authorization. Payloads SHOULD reference large or sensitive artifacts rather +than inline them. Credentials, hidden prompts, and secret tool results MUST NOT +appear in portable envelopes. + +## 9. Conformance + +Tests MUST cover duplicate delivery, out-of-order progress, stale inputs, +handoff causation, escalation, cancellation races, session loss and recovery, +and conflicting terminal results. diff --git a/docs/specification/execution-preferences-v0.1.md b/docs/specification/execution-preferences-v0.1.md new file mode 100644 index 000000000..df1ecbebd --- /dev/null +++ b/docs/specification/execution-preferences-v0.1.md @@ -0,0 +1,94 @@ +# Squad Execution Preferences Profile v0.1 + + + +**Status:** Working Draft + +**Document class:** Normative unless marked informative + +**Profile identifier:** `squad-execution-preferences/v0.1` + +**Maturity:** Experimental normative + +**Depends on:** `squad-core/v0.1`, `squad-interop/v0.1`, `squad-charter/v0.1`, +`squad-configuration-casting/v0.1` + +## 1. Scope and non-goals + +This profile expresses portable execution intent for model selection, reasoning +effort, context tier, automatic selection, and fallback constraints. It does +not standardize vendor catalogs, prices, quality rankings, entitlement, +authorization, or an implementation's selection algorithm. + +## 2. Data model + +An execution preference has `model`, `modelClass`, `reasoningEffort`, +`contextTier`, `fallback`, and `source`. Each value is either absent, `auto`, +or an authored identifier from the applicable vocabulary. Absence means no +preference. `auto` asks the executor to select and MUST NOT be sent to a +provider as a literal model or tier. + +Portable reasoning intent is `low`, `medium`, `high`, `xhigh`, or `max`. +Portable context intent is `default` or `long_context`. Executors MAY support a +subset but MUST make unsupported values observable. + +## 3. Precedence + +The order is explicit operation input, session directive, repository +configuration, charter preference, task-aware automatic policy, then platform +default. A higher layer MAY set `auto`, which delegates selection to lower +runtime policy without erasing the source evidence. + +## 4. Resolution result and states + +Resolution produces `requested`, `selected`, `source`, `fallbackAttempt`, +`availability`, `authorization`, and `policyDisposition`. States are +`unresolved`, `selected`, `unavailable`, `unauthorized`, `policy-denied`, +`fallback-selected`, and `failed`. + +Availability means the runtime can address a model. Authorization means the +actor may use it. Policy disposition means local implementation policy permits +it. These checks are independent and MUST NOT be collapsed into one silent +fallback. + +## 5. Runtime fallback behavior + +Fallback MAY be `none`, `same-provider`, `same-class`, or `platform-default`. +An executor MUST NOT broaden beyond the authored constraint. Every attempt MUST +be recorded. After the configured attempt limit, execution fails explicitly. +Provider-specific chains and current model names are informative implementation +policy, not part of this profile. + +## 6. Capabilities and behavior classes + +Capabilities are `preferences.consume`, `preferences.resolve`, +`preferences.explain`, and `preferences.execute`. Canonical executors preserve +source and disposition evidence. Compatible executors MAY map unsupported +reasoning or context values to provider vocabulary only when the mapping is +declared and loss is reported. + +## 7. Diagnostics, idempotency, and concurrency + +Invalid values, contradictory constraints, unavailable selections, denied +authorization, policy denial, and exhausted fallback are distinct diagnostics. +Resolution is idempotent for the same preference revision, task class, actor, +availability snapshot, and policy revision. A changed snapshot produces a new +resolution ID rather than mutating prior evidence. + +## 8. Versioning and extensions + +Vendor catalogs are discovered out of band and versioned separately. Unknown +namespaced preference fields round-trip. Unsupported mandatory preference +semantics fail closed. + +## 9. Security and privacy + +Preferences never grant provider access, tools, data access, or spending +authority. Diagnostics MUST NOT expose credentials, account entitlements, or +private policy text. + +## 10. Conformance + +Tests MUST cover absence versus `auto`, each precedence layer, unavailable and +unauthorized models, policy denial, constrained fallback, exhausted retries, +and reasoning/context subset mapping. diff --git a/docs/specification/github-work-lifecycle-binding-v0.1.md b/docs/specification/github-work-lifecycle-binding-v0.1.md new file mode 100644 index 000000000..1b9f2444a --- /dev/null +++ b/docs/specification/github-work-lifecycle-binding-v0.1.md @@ -0,0 +1,106 @@ +# Squad GitHub Work Lifecycle Binding v0.1 + +**Status:** Working Draft + +**Document class:** Normative unless marked informative + +**Binding identifier:** `squad-work-github/v0.1` + +**Maturity:** Experimental normative provider binding + +**Depends on:** `squad-interop/v0.1`, `squad-work-lifecycle/v0.1`, +`squad-governance-review/v0.1` + +## 1. Scope and non-goals + +This binding maps portable work resources and transitions to GitHub issues, +labels, assignees, branches or worktrees, pull requests, checks, reviews, +merge, and closure. It does not standardize GitHub UI wording, GraphQL versus +REST versus CLI transport, repository merge policy, or organization rulesets. + +## 2. Resource mapping + +- `work-item` maps to an issue. +- accountable route maps to exactly one `squad:{member-id}` label; human + assignees are provider authorization or notification, not portable ownership. +- `workspace` maps to a branch and optional local worktree. +- `change` maps to a pull request. +- `check` maps to a required status check or check run. +- `review` maps to review decision plus governance evidence. +- `integration` maps to the pull request merge commit or squash/rebase result. +- `closure` maps to issue closed state with an explicit reason. + +## 3. Labels and references + +The binding reserves `squad` and `squad:{member-id}`. Implementations MUST +ensure required labels exist before relying on them. Issue, pull request, +commit, branch, and check references MUST include repository identity. + +Temporary safe-output references MAY be used during one transaction but MUST +resolve before the operation is considered successful. + +## 4. State mapping + +Open without accountable label is `untriaged`; labeled without workspace is +`assigned`; claimed branch is `in-progress`; open pull request is +`needs-review`; `CHANGES_REQUESTED` is `changes-requested`; failing required +checks is `checks-failed`; current-revision approval plus passing required +checks is `ready`; merged is `integrated`; issue closure after merge is `done`. + +Draft pull requests remain `in-progress` unless an extension declares them +reviewable. A closed unmerged pull request is not integrated. + +## 5. Review and merge authority + +GitHub review state alone does not prove reviewer independence or lockout; the +governance evidence record supplies that proof. Branch protection, rulesets, +actor permissions, and merge queue policy remain GitHub authority and MUST be +checked at execution time. + +## 6. Idempotency and concurrency + +Mutation keys SHOULD be stored in issue or pull request evidence. Creation +retries MUST search exact recorded mappings before creating another resource. +Updates use current issue or pull request revision evidence when available. +Merge retries MUST recognize an already merged pull request as the original +successful result. + +## 7. Failure semantics + +Missing labels, inaccessible repositories, rate limits, secondary limits, +ruleset denial, stale head SHA, merge conflicts, unresolved temporary +references, partial issue creation, and webhook reordering are distinct +failures. A binding MUST NOT silently treat label creation or authentication +failure as success. + +## 8. Versioning, extensions, security, and privacy + +This draft assumes GitHub REST API version `2022-11-28`, GraphQL schema behavior +observed on 2026-09-25, and `gh` operations that map to those APIs. A conforming +binding MUST publish its exact supported API versions and observation date. +An unavailable version returns `unsupported-provider-version`; a changed field, +enum, mutation, or ruleset behavior returns `provider-drift` and fails closed +for mutation until revalidated. + +GitHub API versions and this binding version are independent. Namespaced labels +and evidence fields round-trip. Issue bodies and comments are untrusted input. +Tokens, authorization scopes, private check logs, and secret values MUST NOT be +copied into portable records. + +## 9. Language-neutral example + +```json +{ + "profile": "squad-work-github/v0.1", + "workItem": "github:bradygaster/squad#2069", + "change": "github:bradygaster/squad!2074", + "head": "71634fde", + "state": "needs-review" +} +``` + +## 10. Conformance + +Binding tests MUST cover label provisioning, assignment, draft and ready pull +requests, stale-head review, check failure, changes requested, lockout evidence, +merge conflict, already-merged retry, closure, and reordered webhook events. diff --git a/docs/specification/governance-review-v0.1.md b/docs/specification/governance-review-v0.1.md new file mode 100644 index 000000000..d182c344d --- /dev/null +++ b/docs/specification/governance-review-v0.1.md @@ -0,0 +1,117 @@ +# Squad Governance and Review Profile v0.1 + +**Status:** Working Draft + +**Document class:** Normative unless marked informative + +**Profile identifier:** `squad-governance-review/v0.1` + +**Maturity:** Experimental normative + +**Depends on:** `squad-core/v0.1`, `squad-interop/v0.1`, +`squad-team-routing/v0.1` + +**Protocol artifact:** governance evidence records + +## 1. Scope and non-goals + +This profile defines accountable ownership, reviewer independence, blocking +verdict evidence, rejection lockout, reassignment, escalation, approval, and +completion transitions. It does not define code-review style, preferred +feedback wording, repository merge policy, or who is authorized by a provider. + +## 2. Roles and vocabulary + +The **accountable owner** owns the requirement and final handoff. The +**artifact author** produces a revision. A **blocking reviewer** issues +verdicts. A **revision author** independently produces the next revision after +rejection. A **coordinator** validates transitions. An **authority** resolves +deadlock or policy disputes. + +## 3. Evidence record + +Every transition MUST use the shared evidence envelope conforming to +`test-fixtures/spec/suite-v0.1/evidence.schema.json`. Governance data includes +profile, event ID, artifact ID and revision, state, accountable owner, author, +reviewer, blockers, lockout set, prior event, actor, timestamp, and +idempotency key. + +## 4. State machine + +The deterministic states are: + +`draft -> submitted -> under-review -> approved -> completed` + +Rejection uses: + +`under-review -> rejected -> reassignment-required -> revising -> submitted` + +Escalation uses: + +`rejected|reassignment-required -> escalated -> dispositioned` + +Only an approved artifact with applicable external checks may become completed. +Completion does not imply provider merge unless a binding says so. + +## 5. Blocking verdict contract + +A rejection MUST identify artifact location, violated requirement or invariant, +reproducible evidence or concrete failure scenario, and the minimum clearing +condition. Preference-only feedback MUST be advisory and MUST NOT trigger +lockout. + +## 6. Reviewer independence and lockout state machine + +The blocking reviewer MUST differ from the current artifact author. After a +rejection, the current author is added to the artifact revision-cycle lockout +set and MUST NOT author, co-author, pair on, or direct the next revision. The +reviewer MAY clarify the blocker but MUST NOT become revision author for the +same cycle. The coordinator MUST select an eligible revision author not in the +lockout set. + +Lockout is scoped to the artifact and persists until a later revision is +approved or an authority records an explicit override. A rejected revision +adds its author to the set. If no eligible author remains, the coordinator MUST +escalate rather than silently readmit a locked-out author. + +The separately testable lockout states are `clear`, `author-locked`, +`revision-assigned`, `revision-submitted`, `cleared`, and `deadlocked`. + +This resolves a repository inconsistency: `.squad/governance.md` currently +assigns immediate revision preparation to the accountable owner, while the +reviewer protocol requires independent revision. Portable conformance follows +the stricter auditable rule; implementations using owner self-revision are +nonconforming until they declare a separate compatibility policy. + +## 7. Approval and reassignment + +Approval MUST identify the reviewed revision and blocker disposition. A +reviewer MUST NOT approve a revision they authored. Reassignment changes the +revision author, not the accountable owner, unless an authority separately +records ownership transfer. + +## 8. Failure, idempotency, and concurrency + +Invalid transitions, self-review, locked-out revision authors, missing blocker +evidence, stale artifact revisions, and duplicate event IDs fail closed. +Repeating an idempotency key returns the original result. Concurrent events use +the prior event ID and artifact revision as compare-and-swap conditions. + +## 9. Versioning and extensions + +Consumers MUST reject unsupported profile versions and unknown mandatory +states. Namespaced evidence fields are preserved. New advisory event types MAY +be added compatibly; changing transition eligibility requires a new profile. + +## 10. Security, privacy, and authority + +Governance evidence does not grant repository permissions. Actors and +reviewers MUST be authenticated by the executing environment. Records SHOULD +reference evidence rather than duplicate secrets, private logs, or personal +data. + +## 11. Conformance + +Conformance requires fixtures for approval, advisory feedback, rejection, +lockout, repeated rejection, reassignment, escalation, deadlock, stale events, +duplicate retries, and external-check gating. diff --git a/docs/specification/interoperability-conventions-v0.1.md b/docs/specification/interoperability-conventions-v0.1.md new file mode 100644 index 000000000..b26d4368c --- /dev/null +++ b/docs/specification/interoperability-conventions-v0.1.md @@ -0,0 +1,131 @@ +# Squad Interoperability Conventions v0.1 + + + +**Status:** Working Draft + +**Document class:** Normative + +**Capability identifier:** `squad-interop/v0.1` + +**Maturity:** Stable normative dependency + +**Depends on:** `squad-core/v0.1` + +## 1. Scope + +These conventions define discovery, conformance claims, authority identity, +version negotiation, shared evidence, concurrency, duplicate suppression, and +privacy rules reused by every profile and provider binding. Profiles MUST +reference these definitions rather than invent local alternatives. + +## 2. Suite discovery and capability claims + +An implementation MUST expose a discovery document or API result containing: + +- supported core, convention, profile, and binding identifiers; +- exact capability identifiers and operations; +- supported artifact or message schema versions; +- canonical, compatible, and migration-only behavior classes; +- extension namespaces owned or understood; +- provider assumptions and implementation-policy boundaries. + +Discovery is descriptive, not authorization. Unsupported mandatory versions or +capabilities MUST fail closed. Implementations MUST NOT infer nearest-version +compatibility. + +## 3. Stable identity and authority + +Every actor, member, service, and artifact has an immutable UID scoped by an +issuer. Display names, member IDs used as paths, roles, casting personas, and +provider logins are attributes and MUST NOT be treated as the authority UID. + +An authority decision MUST identify actor UID, issuer, authenticated provider +identity, authority basis, action, resource UID, and policy revision. Casting +or persona changes MUST NOT alter authority. + +## 4. Shared event and evidence envelope + +Normative events and evidence MUST conform to +`test-fixtures/spec/suite-v0.1/evidence.schema.json`. The envelope carries: + +- specification and schema identifiers; +- event ID, type, source, subject, and time; +- actor UID and authority evidence when an action is asserted; +- correlation, causation, idempotency, and replay keys; +- resource UID, revision, expected prior revision, and result revision; +- payload digest, redaction declaration, extensions, and typed data. + +Profile data belongs under `data`; profiles MUST NOT redefine envelope fields. +Evidence is immutable. Corrections are new events linked by causation. + +## 5. Concurrency, retry, and replay + +Mutations MUST declare an idempotency key. Operations on revisioned resources +MUST declare the expected prior revision or explicitly state that no revision +precondition is available. A mismatch returns `conflict` without partial +mutation. + +At-least-once delivery is the default. Consumers MUST suppress exact duplicate +event IDs and return the original result for repeated idempotency keys. The +same idempotency key with different payload digest is an error. Replay windows, +leases, and retry limits MUST be declared by the executing capability. + +## 6. Canonical privacy and secrets exclusion + +Portable artifacts and evidence MUST NOT contain credentials, access tokens, +private keys, session cookies, secret environment values, hidden prompts, +unredacted sensitive tool output, or provider authorization headers. + +Producers MUST declare one of `none`, `metadata-only`, or `content-redacted` in +the envelope's redaction field. Sensitive source material SHOULD be referenced +by an access-controlled URI and digest. Diagnostics MUST identify the excluded +field or class without reproducing secret content. + +Profiles MAY impose stricter privacy rules. They MUST NOT weaken this section. + +## 7. Extension ownership + +Extension keys use a namespace controlled by their owner. The `squad` +namespace is reserved. Discovery MUST identify recognized namespaces and +whether each is advisory or mandatory. Unknown advisory extensions round-trip; +unknown mandatory extensions fail closed. + +## 8. Version negotiation and drift + +Negotiation selects exact identifiers and schema versions before semantic +processing. Provider bindings MUST also declare the provider API or behavior +versions assumed. Runtime drift outside the declared range returns +`unsupported-provider-version` or `provider-drift`; it MUST NOT be silently +treated as compatible. + +## 9. Conformance classes + +Capabilities claim one or more classes: producer, consumer, editor, validator, +executor, recorder, or provider binding. Claims are operation-specific and +MUST include canonical versus compatible behavior. + +The portable suite uses the externally claimable classes `artifact-only`, +`validator`, `editor`, `state-machine-executor`, `provider-binding`, and +`runtime`. The suite index at +`test-fixtures/spec/suite-v0.1/index.json` records exact profile-prefixed +capabilities, dependencies, dialect, maturity, manifests, and schemas. Partial +claims are valid only when all applicable cases and dependency claims pass. + +Portable case kinds are `positive`, `negative`, `compatibility`, `legacy`, +`round-trip`, `mutation`, `transition`, `replay`, `concurrency`, `provider`, +and `security`. Runtime profiles SHOULD promote from requirements-only status +using transition traces with injected clocks and IDs. Provider bindings SHOULD +reference neutral case IDs and digests rather than duplicate lifecycle rules. + +Every normative profile requires falsifiable fixtures and an independent +reviewer before publication. Passing a schema alone does not prove executor or +authority conformance. + +## 10. Precedents + +These conventions align conceptually with W3C conformance classes, Kubernetes +UID/name and discovery separation, OCI media-type negotiation, RFC 9110 +idempotency semantics, CloudEvents 1.0.2, in-toto/SLSA provenance, and JSON +Schema vocabularies. Those precedents are informative unless cited by a profile +as a normative reference. diff --git a/docs/specification/planning-activation-v0.1.md b/docs/specification/planning-activation-v0.1.md new file mode 100644 index 000000000..ec7ffab7c --- /dev/null +++ b/docs/specification/planning-activation-v0.1.md @@ -0,0 +1,100 @@ +# Squad Planning and Activation Profile v0.1 + + + +**Status:** Working Draft + +**Document class:** Normative unless marked informative + +**Profile identifier:** `squad-planning-activation/v0.1` + +**Maturity:** Experimental normative + +**Depends on:** `squad-core/v0.1`, `squad-interop/v0.1`, +`squad-team-routing/v0.1`, +`squad-work-lifecycle/v0.1` + +## 1. Scope and non-goals + +This profile defines PRD or intent intake, research, triage, program planning, +implementation decomposition, validation, acceptance, activation, and +revision evidence. It does not standardize prompt text, issue-comment wording, +estimation culture, or one provider's safe-output mechanism. + +## 2. Artifact envelope + +Each artifact MUST declare profile, kind, schema version, intent ID, artifact +ID, revision, predecessor IDs, producer, timestamp, content digest, and +idempotency key. Portable kinds are `intent`, `research`, `triage`, `program`, +`implementation`, `validation`, `scope-acceptance`, +`implementation-acceptance`, `activation`, and `lifecycle-state`. + +An intent MAY reference inline content, a file, or a URL. Consumers MUST record +the content digest actually evaluated. A changed digest creates a new intent +revision and invalidates dependent acceptance until revalidated. + +## 3. State transitions + +`idle -> researching -> triaging -> program-planning -> +implementation-planning -> validating -> scope-accepted -> +implementation-accepted -> activated` + +Revision is allowed before activation and MUST identify artifacts superseded. +A validation failure returns to implementation planning. Acceptance MUST name +the exact accepted revision. Activation requires both acceptances and a passing +validation over the same dependency closure. + +## 4. Decomposition and acceptance + +Implementation tasks MUST have stable task IDs, acceptance criteria, dependency +IDs, accountable route, and activation disposition. Sizing, milestones, and +meeting style are informative unless selected by an extension. + +Human or delegated authority MUST be recorded for acceptance. Mere artifact +existence, model output, or a passing validator MUST NOT be treated as human +approval. + +PRD prose quality, completeness, and preferred decomposition style are +informative. Artifact identities, revisions, traceability links, dependencies, +acceptance, and activation transitions are normative. + +## 5. Activation + +Activation maps accepted task IDs to provider work-item references. Partial +activation MUST record the selected phase and remaining tasks. A task MUST NOT +be created twice for the same activation idempotency key. Unresolved temporary +references fail closed. + +## 6. Capabilities and roles + +Capabilities are `planning.produce`, `planning.validate`, +`planning.accept-scope`, `planning.accept-implementation`, and +`planning.activate`. Producers create artifacts; validators check structure +and dependency closure; acceptors exercise authority; activators create work. + +## 7. Diagnostics and failure semantics + +Missing intent, stale digest, unknown dependency, cyclic dependency, unroutable +task, failed validation, mismatched acceptance revision, unresolved reference, +capacity limit, and partial provider mutation are distinct failures. Partial +mutation MUST return created references and a resumable boundary. + +## 8. Idempotency, retry, and concurrency + +Draft stages are replaceable revisions, not destructive edits. Acceptance +records are immutable. Activation uses compare-and-swap on accepted revisions +and provider idempotency keys. Concurrent activation attempts MUST converge on +one mapping or fail with an explicit conflict. + +## 9. Versioning, extensions, and security + +Artifact schema versions negotiate independently. Namespaced kinds and fields +round-trip. Intake content is untrusted; links, commands, and embedded prompts +MUST NOT execute merely because they appear in a PRD. Secrets SHOULD be +referenced, not copied into artifacts. + +## 10. Conformance + +Fixtures MUST cover granular and fast paths, PRD changes, validation failure, +acceptance revision mismatch, partial activation, duplicate retry, concurrent +activation, and dependency cycles. diff --git a/docs/specification/state-memory-requirements-v0.1.md b/docs/specification/state-memory-requirements-v0.1.md new file mode 100644 index 000000000..aab17e4a5 --- /dev/null +++ b/docs/specification/state-memory-requirements-v0.1.md @@ -0,0 +1,81 @@ +# Squad State and Memory Requirements v0.1 + +**Status:** Informative Requirements Draft + +**Document class:** Informative; not a conformance profile + +**Requirements identifier:** `squad-state-memory-requirements/v0.1` + +**Maturity:** Informative and volatile + +**Depends on:** `squad-core/v0.1`, `squad-interop/v0.1` + +## 1. Problem statement + +Squad currently supports worktree files, git-native orphan references, +two-layer state, external stubs, storage providers, memory classifications, and +provider-specific search. These implementations expose useful seams, but they +do not yet share enough observable recovery and authority evidence for a +truthful normative schema. + +## 2. Required future semantics + +A promoted profile needs to define: + +- one selected backend and explicit authority for each state collection; +- stable collection and record identities independent of backend paths; +- persistence, durability, and visibility guarantees; +- compare-and-swap revisions and conflict evidence; +- append, replace, delete, archive, and tombstone semantics; +- startup verification, degraded mode, circuit breaking, and recovery; +- migration and rollback between backends without split-brain authority; +- memory classification before provider dispatch; +- provider availability, search provenance, deletion, retention, and audit; +- trust boundaries among repository state, host-injected services, and remote + providers; +- encryption, secrets, privacy, redaction, and data residency declarations. + +## 3. Provisional authority rules + +Static configuration and normative artifacts remain file-authoritative unless a +profile says otherwise. Mutable state MUST have exactly one authority backend +per collection. Mirrors and caches MUST identify their source revision and MUST +NOT accept writes while authority is ambiguous. + +Memory classification MUST precede durable or external writes. Forbidden and +transient content MUST NOT reach a durable provider. Provider failures MUST be +observable and MUST NOT silently convert a denied write into local success. + +## 4. Candidate portable operations + +The likely operation set is `read`, `list`, `compare-and-swap-write`, `append`, +`delete`, `archive`, `health`, `migrate`, `recover`, `classify`, `search`, and +`promote`. Each operation needs request IDs, actor, expected revision, result +revision, backend identity, diagnostics, and idempotency semantics. + +## 5. Security and privacy requirements + +Backends must prevent path traversal and unauthorized ref access. Audit records +should omit content by default. External providers require explicit enablement, +authorization, retention disclosure, deletion behavior, and secret filtering. +Recovery tools must not leak state from another repository, worktree, user, or +account. + +## 6. Promotion criteria + +This document may become `squad-state-memory/v0.1` only after: + +1. language-neutral fixtures exercise the same collection operations against + local files, orphan refs, two-layer state, and one external provider; +2. compare-and-swap, duplicate retry, crash recovery, migration, rollback, and + split-brain tests have common expected results; +3. authority discovery and degraded-mode behavior are independently specified; +4. memory classification and provider dispatch have stable vocabularies; +5. a security review covers traversal, ref isolation, secrets, retention, + deletion, and cross-tenant leakage. + +## 7. Intentionally deferred details + +Git plumbing, SQLite layout, index algorithms, embeddings, ranking, cache +format, circuit-breaker constants, prompt-only fallback, and provider product +names remain implementation policy. diff --git a/docs/specification/team-routing-v0.1.md b/docs/specification/team-routing-v0.1.md new file mode 100644 index 000000000..381d99bb0 --- /dev/null +++ b/docs/specification/team-routing-v0.1.md @@ -0,0 +1,103 @@ +# Squad Team and Routing Profile v0.1 + +**Status:** Working Draft + +**Document class:** Normative unless marked informative + +**Profile identifier:** `squad-team-routing/v0.1` + +**Maturity:** Stable normative + +**Depends on:** `squad-core/v0.1`, `squad-interop/v0.1`, +`squad-charter/v0.1` + +**Artifacts:** `team.md`, `routing.md` + +**Feedback:** Issue #2069 + +BCP 14 requirement keywords apply only when written in all capitals. + +## 1. Scope and non-goals + +This profile defines roster identity, accountable roles, capability ownership, +route selection evidence, and joins to charter and casting identities. It does +not standardize persona prose, prompt construction, task-classification +algorithms, or a preferred organization shape. + +## 2. Vocabulary and roles + +A **member ID** is the lowercase kebab-case path identity from Charter v0.1. A +**role** is a human-readable accountable function. A **route** maps one work +type to one primary member. Producers maintain artifacts; consumers resolve +ownership; editors preserve unknown extensions; validators check joins; a +coordinator executes route decisions. + +Authority uses the immutable actor UID from Interoperability Conventions, not a +member ID, display name, role, or casting persona. + +## 3. Canonical artifacts + +`team.md` MUST contain a `Members` table with `ID`, `Role`, `Charter`, and +`Status` semantics. Compatible headings and columns MAY vary, but validators +MUST expose their mapping. Every active member MUST reference exactly one +existing charter whose identity matches the member ID. + +`routing.md` MUST contain a `Routing Table` with `Work Type`, `Route To`, and +optional `Examples`. `Route To` MUST name exactly one active member. Support +identities and coding agents MAY appear in the roster but MUST NOT be routing +destinations unless the team manifest explicitly grants accountable ownership. + +The suite index declares artifact-only conformance until a profile-specific +manifest supplies document, mutation, and route-resolution cases. + +## 4. Deterministic routing + +1. An explicit, authorized member assignment wins. +2. Otherwise the highest-priority matching route wins. +3. Equal-priority matches are an ambiguity failure; consumers MUST NOT select + by file order unless the profile extension declares that policy. +4. No match uses the declared fallback member. +5. No fallback produces `route-unresolved` and requires coordinator or human + disposition. + +Classifiers MAY use different algorithms, but route evidence MUST identify the +selected rule, input revision, confidence, and fallback use. Generated regexes, +embeddings, prompts, and ranking algorithms are non-normative. + +## 5. Capabilities and behavior classes + +Capabilities are `team.produce`, `team.consume`, `team.edit`, `team.validate`, +`routing.resolve`, and `routing.explain`. Canonical producers emit one active +member row per ID and one primary owner per work type. Compatible consumers MAY +accept legacy display-name destinations if they resolve unambiguously. + +## 6. Diagnostics and failures + +Validators MUST report stable diagnostics for duplicate IDs, missing charters, +identity mismatch, unknown destinations, inactive destinations, duplicate work +types, ambiguous priorities, and absent fallback. Invalid topology MUST fail +closed for execution while remaining available for diagnostic display. + +## 7. Idempotency, retry, and concurrency + +Applying the same roster or route revision is idempotent. Editors MUST use a +revision or content digest for concurrent updates and MUST reject stale writes. +Resolution is read-only and MAY retry after reloading a newer revision. + +## 8. Versioning, extensions, and discovery + +Profile selection is explicit. `X-` Markdown sections and namespaced topology +properties are extensions and MUST round-trip unchanged. Consumers MUST fail +closed on unsupported profile versions and unknown mandatory capabilities. + +## 9. Security, privacy, and authority + +Roster membership and routing do not grant provider authorization, repository +write access, tools, secrets, or model entitlement. Artifact text is untrusted. +Producers SHOULD avoid personal data in roles and examples. + +## 10. Conformance + +Conformance requires schema-valid topology fixtures, charter joins, ambiguity +fixtures, stale-revision tests, and deterministic route evidence. A resolver +MUST publish the exact capabilities it implements. diff --git a/docs/specification/work-lifecycle-v0.1.md b/docs/specification/work-lifecycle-v0.1.md new file mode 100644 index 000000000..1122d9ae1 --- /dev/null +++ b/docs/specification/work-lifecycle-v0.1.md @@ -0,0 +1,89 @@ +# Squad Work Lifecycle Profile v0.1 + + + +**Status:** Working Draft + +**Document class:** Normative unless marked informative + +**Profile identifier:** `squad-work-lifecycle/v0.1` + +**Maturity:** Experimental normative + +**Depends on:** `squad-core/v0.1`, `squad-interop/v0.1`, +`squad-team-routing/v0.1`, +`squad-governance-review/v0.1` + +## 1. Scope and non-goals + +This profile defines a provider-neutral lifecycle for work items, assignment, +dependencies, branches or workspaces, change requests, checks, reviews, merge, +closure, and cleanup. It does not require Git, GitHub, one branch naming +scheme, one merge strategy, or automatic merge. + +## 2. Resource model + +Portable resources are `work-item`, `dependency`, `assignment`, `workspace`, +`change`, `check`, `review`, `integration`, and `closure`. Each resource has a +provider-qualified reference, immutable identity, observed revision, and +authority source. + +The closed finite work states are `untriaged`, `assigned`, `in-progress`, `needs-review`, +`changes-requested`, `checks-failed`, `ready`, `integrated`, `done`, +`cancelled`, and `blocked`. + +All transitions not explicitly listed by this profile are invalid. + +## 3. Deterministic transitions + +- `untriaged -> assigned` requires one accountable route. +- `assigned -> in-progress` requires a claimed workspace. +- `in-progress -> needs-review` requires a change reference and evidence. +- `needs-review -> changes-requested` follows a blocking governance verdict. +- `needs-review|changes-requested|checks-failed -> ready` requires approval of + the current revision and all required checks passing. +- `ready -> integrated` requires provider integration evidence. +- `integrated -> done` requires closure disposition and dependency updates. + +Provider observations MAY arrive out of order. Consumers MUST reconcile by +resource revision and MUST NOT infer integration from closure alone. + +## 4. Dependencies and workspaces + +Dependencies have stable IDs and `blocks` direction. Cycles are invalid unless +an extension explicitly permits informational relationships. A workspace claim +records base revision, branch or workspace reference, actor, and lease. +Worktrees are one compatible workspace implementation, not a requirement. + +## 5. Capabilities and roles + +Capabilities are `work.observe`, `work.assign`, `work.claim`, +`work.change-submit`, `work.check`, `work.review`, `work.integrate`, and +`work.close`. Provider bindings declare which operations they implement. + +## 6. Failure and diagnostics + +Unroutable assignment, dependency cycle, stale workspace base, duplicate active +claim, missing change, stale review, failed required check, unauthorized merge, +partial integration, and closure mismatch are distinct failures. Failures MUST +retain provider references and recovery guidance. + +## 7. Idempotency, retry, and concurrency + +Every mutation carries an operation ID. Repeated operations return the same +resource mapping. Workspace and integration operations require expected +revisions. Partial provider mutations are resumable and MUST NOT be reported as +success-shaped completion. + +## 8. Versioning, extensions, security, and privacy + +Bindings negotiate separately from this profile. Namespaced states and fields +round-trip. Work descriptions, review comments, branches, and check logs are +untrusted. Provider credentials and secret logs MUST NOT enter lifecycle +records. + +## 9. Conformance + +Conformance requires traces for happy path, blocked dependencies, stale base, +review rejection and lockout, check failure and recovery, duplicate retries, +partial integration, cancellation, and provider event reordering. diff --git a/test-fixtures/spec/suite-v0.1/evidence.schema.json b/test-fixtures/spec/suite-v0.1/evidence.schema.json new file mode 100644 index 000000000..e7192fb7b --- /dev/null +++ b/test-fixtures/spec/suite-v0.1/evidence.schema.json @@ -0,0 +1,97 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://squad.dev/spec/squad-interop/v0.1/evidence.schema.json", + "title": "Squad Shared Event and Evidence Envelope v0.1", + "type": "object", + "additionalProperties": false, + "required": [ + "specversion", + "schema", + "id", + "type", + "source", + "subject", + "time", + "actor", + "resource", + "correlationId", + "causationId", + "idempotencyKey", + "payloadDigest", + "redaction", + "data" + ], + "properties": { + "specversion": { "const": "squad-interop/v0.1" }, + "schema": { + "type": "string", + "pattern": "^squad-[a-z0-9-]+/v(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)$" + }, + "id": { "type": "string", "format": "uuid" }, + "type": { + "type": "string", + "pattern": "^squad\\.[a-z0-9-]+\\.[a-z0-9.-]+$" + }, + "source": { "type": "string", "format": "uri-reference" }, + "subject": { "type": "string", "minLength": 1 }, + "time": { "type": "string", "format": "date-time" }, + "actor": { + "type": "object", + "additionalProperties": false, + "required": ["uid", "issuer", "providerIdentity", "authorityBasis"], + "properties": { + "uid": { "type": "string", "minLength": 1 }, + "issuer": { "type": "string", "minLength": 1 }, + "providerIdentity": { "type": "string", "minLength": 1 }, + "authorityBasis": { "type": "string", "minLength": 1 } + } + }, + "resource": { + "type": "object", + "additionalProperties": false, + "required": ["uid", "revision"], + "properties": { + "uid": { "type": "string", "minLength": 1 }, + "revision": { + "anyOf": [ + { "type": "string", "minLength": 1 }, + { "type": "integer" } + ] + }, + "expectedRevision": { + "anyOf": [ + { "type": "string", "minLength": 1 }, + { "type": "integer" }, + { "type": "null" } + ] + }, + "resultRevision": { + "anyOf": [ + { "type": "string", "minLength": 1 }, + { "type": "integer" }, + { "type": "null" } + ] + } + } + }, + "correlationId": { "type": "string", "format": "uuid" }, + "causationId": { + "anyOf": [ + { "type": "string", "format": "uuid" }, + { "type": "null" } + ] + }, + "idempotencyKey": { "type": "string", "minLength": 1, "maxLength": 256 }, + "replayKey": { "type": "string", "minLength": 1, "maxLength": 256 }, + "payloadDigest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "redaction": { "enum": ["none", "metadata-only", "content-redacted"] }, + "extensions": { + "type": "object", + "propertyNames": { "pattern": "^[a-z0-9]+:[a-z0-9][a-z0-9.-]*$" } + }, + "data": { "type": "object" } + } +} diff --git a/test-fixtures/spec/suite-v0.1/governance-rejection.example.json b/test-fixtures/spec/suite-v0.1/governance-rejection.example.json new file mode 100644 index 000000000..748025594 --- /dev/null +++ b/test-fixtures/spec/suite-v0.1/governance-rejection.example.json @@ -0,0 +1,45 @@ +{ + "specversion": "squad-interop/v0.1", + "schema": "squad-governance-review/v0.1", + "id": "1416ae81-91ef-48b3-b6fa-9b810b86b012", + "type": "squad.governance.rejected", + "source": "github:bradygaster/squad", + "subject": "artifact:change-request/2074", + "time": "2026-09-25T14:17:33Z", + "actor": { + "uid": "squad-member:unit-contract-tester", + "issuer": "squad:bradygaster/squad", + "providerIdentity": "github:reviewer-login", + "authorityBasis": "review-gate:contract" + }, + "resource": { + "uid": "artifact:change-request/2074", + "revision": "71634fde", + "expectedRevision": "71634fde", + "resultRevision": "71634fde" + }, + "correlationId": "8d1f7875-b855-4996-93fd-fee67c775188", + "causationId": null, + "idempotencyKey": "review:2074:71634fde:unit-contract-tester", + "replayKey": "github-review:2074:71634fde:1", + "payloadDigest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "redaction": "metadata-only", + "extensions": {}, + "data": { + "state": "rejected", + "lockoutState": "author-locked", + "accountableOwnerUid": "squad-member:architect", + "authorUid": "squad-member:architect", + "reviewerUid": "squad-member:unit-contract-tester", + "lockoutUids": ["squad-member:architect"], + "blockers": [ + { + "code": "missing-conformance-fixture", + "location": "test-fixtures/spec/suite-v0.1", + "requirement": "Every normative profile has validator or fixture evidence", + "evidence": "No shared envelope fixture was present", + "clearCondition": "Add and validate one language-neutral evidence fixture" + } + ] + } +} diff --git a/test-fixtures/spec/suite-v0.1/index.json b/test-fixtures/spec/suite-v0.1/index.json new file mode 100644 index 000000000..6e77a9369 --- /dev/null +++ b/test-fixtures/spec/suite-v0.1/index.json @@ -0,0 +1,188 @@ +{ + "$schema": "./index.schema.json", + "suite": "squad-conformance/v0.1", + "schemaVersion": 1, + "publication": { + "status": "working-draft", + "revision": null, + "digest": null + }, + "dialects": [ + { + "id": "charter-v0-1-legacy", + "description": "Existing Charter v0.1 manifest dialect, registered unchanged." + }, + { + "id": "suite-v0-1", + "description": "Reusable suite index and shared event/evidence schemas." + }, + { + "id": "requirements-only", + "description": "No portable executable manifest yet; promotion criteria are normative planning input only." + } + ], + "sharedSchemas": [ + "test-fixtures/spec/suite-v0.1/evidence.schema.json" + ], + "profiles": [ + { + "id": "squad-core/v0.1", + "maturity": "stable-normative", + "spec": "docs/specification/core-v0.1.md", + "dialect": "requirements-only", + "dependencies": [], + "classes": ["artifact-only"], + "capabilities": ["squad-core/v0.1#consume"], + "manifest": null, + "manifestSchema": null + }, + { + "id": "squad-interop/v0.1", + "maturity": "stable-normative", + "spec": "docs/specification/interoperability-conventions-v0.1.md", + "dialect": "suite-v0-1", + "dependencies": ["squad-core/v0.1"], + "classes": ["artifact-only", "validator"], + "capabilities": ["squad-interop/v0.1#evidence.validate"], + "manifest": null, + "manifestSchema": null + }, + { + "id": "squad-charter/v0.1", + "maturity": "stable-normative", + "spec": "docs/specification/charter-v0.1.md", + "dialect": "charter-v0-1-legacy", + "dependencies": ["squad-core/v0.1"], + "classes": ["artifact-only", "validator", "editor", "runtime"], + "capabilities": [ + "squad-charter/v0.1#validate", + "squad-charter/v0.1#parse", + "squad-charter/v0.1#serialize", + "squad-charter/v0.1#runtime-compile" + ], + "manifest": "test-fixtures/spec/charter-v0.1/manifest.json", + "manifestSchema": "test-fixtures/spec/charter-v0.1/manifest.schema.json" + }, + { + "id": "squad-team-routing/v0.1", + "maturity": "stable-normative", + "spec": "docs/specification/team-routing-v0.1.md", + "dialect": "requirements-only", + "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-charter/v0.1"], + "classes": ["artifact-only"], + "capabilities": ["squad-team-routing/v0.1#artifact"], + "manifest": null, + "manifestSchema": null + }, + { + "id": "squad-configuration-casting/v0.1", + "maturity": "experimental-normative", + "spec": "docs/specification/configuration-casting-v0.1.md", + "dialect": "requirements-only", + "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-team-routing/v0.1"], + "classes": ["artifact-only"], + "capabilities": ["squad-configuration-casting/v0.1#artifact"], + "manifest": null, + "manifestSchema": null + }, + { + "id": "squad-governance-review/v0.1", + "maturity": "experimental-normative", + "spec": "docs/specification/governance-review-v0.1.md", + "dialect": "suite-v0-1", + "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-team-routing/v0.1"], + "classes": ["state-machine-executor"], + "capabilities": ["squad-governance-review/v0.1#transition"], + "manifest": "test-fixtures/spec/suite-v0.1/governance-rejection.example.json", + "manifestSchema": "test-fixtures/spec/suite-v0.1/evidence.schema.json" + }, + { + "id": "squad-execution-preferences/v0.1", + "maturity": "experimental-normative", + "spec": "docs/specification/execution-preferences-v0.1.md", + "dialect": "requirements-only", + "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-charter/v0.1", "squad-configuration-casting/v0.1"], + "classes": ["artifact-only"], + "capabilities": ["squad-execution-preferences/v0.1#artifact"], + "manifest": null, + "manifestSchema": null + }, + { + "id": "squad-work-lifecycle/v0.1", + "maturity": "experimental-normative", + "spec": "docs/specification/work-lifecycle-v0.1.md", + "dialect": "requirements-only", + "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-team-routing/v0.1", "squad-governance-review/v0.1"], + "classes": ["state-machine-executor"], + "capabilities": ["squad-work-lifecycle/v0.1#transition"], + "manifest": null, + "manifestSchema": null + }, + { + "id": "squad-work-github/v0.1", + "maturity": "experimental-normative", + "spec": "docs/specification/github-work-lifecycle-binding-v0.1.md", + "dialect": "requirements-only", + "dependencies": ["squad-interop/v0.1", "squad-work-lifecycle/v0.1", "squad-governance-review/v0.1"], + "classes": ["provider-binding"], + "capabilities": ["squad-work-github/v0.1#binding"], + "manifest": null, + "manifestSchema": null + }, + { + "id": "squad-planning-activation/v0.1", + "maturity": "experimental-normative", + "spec": "docs/specification/planning-activation-v0.1.md", + "dialect": "requirements-only", + "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-team-routing/v0.1", "squad-work-lifecycle/v0.1"], + "classes": ["state-machine-executor"], + "capabilities": ["squad-planning-activation/v0.1#transition"], + "manifest": null, + "manifestSchema": null + }, + { + "id": "squad-ceremony/v0.1", + "maturity": "experimental-normative", + "spec": "docs/specification/ceremony-v0.1.md", + "dialect": "requirements-only", + "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-team-routing/v0.1"], + "classes": ["state-machine-executor"], + "capabilities": ["squad-ceremony/v0.1#transition"], + "manifest": null, + "manifestSchema": null + }, + { + "id": "squad-coordination-handoff/v0.1", + "maturity": "experimental-normative", + "spec": "docs/specification/coordination-handoff-v0.1.md", + "dialect": "requirements-only", + "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-team-routing/v0.1"], + "classes": ["state-machine-executor"], + "capabilities": ["squad-coordination-handoff/v0.1#transition"], + "manifest": null, + "manifestSchema": null + }, + { + "id": "squad-automation/v0.1", + "maturity": "experimental-normative", + "spec": "docs/specification/automation-v0.1.md", + "dialect": "requirements-only", + "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-coordination-handoff/v0.1"], + "classes": ["state-machine-executor"], + "capabilities": ["squad-automation/v0.1#transition"], + "manifest": null, + "manifestSchema": null + }, + { + "id": "squad-state-memory-requirements/v0.1", + "maturity": "informative-volatile", + "spec": "docs/specification/state-memory-requirements-v0.1.md", + "dialect": "requirements-only", + "dependencies": ["squad-core/v0.1", "squad-interop/v0.1"], + "classes": [], + "capabilities": [], + "manifest": null, + "manifestSchema": null + } + ] +} diff --git a/test-fixtures/spec/suite-v0.1/index.schema.json b/test-fixtures/spec/suite-v0.1/index.schema.json new file mode 100644 index 000000000..a3e671fd7 --- /dev/null +++ b/test-fixtures/spec/suite-v0.1/index.schema.json @@ -0,0 +1,142 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://squad.dev/spec/squad-conformance/v0.1/index.schema.json", + "title": "Squad Conformance Suite Index v0.1", + "type": "object", + "additionalProperties": false, + "required": ["suite", "schemaVersion", "publication", "dialects", "sharedSchemas", "profiles"], + "properties": { + "$schema": { "type": "string", "minLength": 1 }, + "suite": { "const": "squad-conformance/v0.1" }, + "schemaVersion": { "const": 1 }, + "publication": { + "type": "object", + "additionalProperties": false, + "required": ["status", "revision", "digest"], + "properties": { + "status": { "enum": ["working-draft", "published"] }, + "revision": { + "anyOf": [ + { "type": "string", "pattern": "^[a-f0-9]{40}$" }, + { "type": "null" } + ] + }, + "digest": { + "anyOf": [ + { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }, + { "type": "null" } + ] + } + }, + "allOf": [ + { + "if": { "properties": { "status": { "const": "published" } } }, + "then": { + "properties": { + "revision": { "type": "string" }, + "digest": { "type": "string" } + } + } + } + ] + }, + "dialects": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/dialect" } + }, + "sharedSchemas": { + "type": "array", + "items": { "$ref": "#/$defs/repoPath" }, + "uniqueItems": true + }, + "profiles": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/profile" } + } + }, + "$defs": { + "repoPath": { + "type": "string", + "minLength": 1, + "not": { "pattern": "(^/|(^|/)\\.\\.(/|$)|\\\\)" } + }, + "profileId": { + "type": "string", + "pattern": "^squad-[a-z0-9-]+/v(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)$" + }, + "capability": { + "type": "string", + "pattern": "^squad-[a-z0-9-]+/v(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)#[a-z0-9.-]+$" + }, + "dialect": { + "type": "object", + "additionalProperties": false, + "required": ["id", "description"], + "properties": { + "id": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, + "description": { "type": "string", "minLength": 1 } + } + }, + "profile": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "maturity", + "spec", + "dialect", + "dependencies", + "classes", + "capabilities", + "manifest", + "manifestSchema" + ], + "properties": { + "id": { "$ref": "#/$defs/profileId" }, + "maturity": { + "enum": ["stable-normative", "experimental-normative", "informative-volatile"] + }, + "spec": { "$ref": "#/$defs/repoPath" }, + "dialect": { "type": "string", "minLength": 1 }, + "dependencies": { + "type": "array", + "items": { "$ref": "#/$defs/profileId" }, + "uniqueItems": true + }, + "classes": { + "type": "array", + "items": { + "enum": [ + "artifact-only", + "validator", + "editor", + "state-machine-executor", + "provider-binding", + "runtime" + ] + }, + "uniqueItems": true + }, + "capabilities": { + "type": "array", + "items": { "$ref": "#/$defs/capability" }, + "uniqueItems": true + }, + "manifest": { + "anyOf": [ + { "$ref": "#/$defs/repoPath" }, + { "type": "null" } + ] + }, + "manifestSchema": { + "anyOf": [ + { "$ref": "#/$defs/repoPath" }, + { "type": "null" } + ] + } + } + } + } +} diff --git a/test/specification-suite.test.ts b/test/specification-suite.test.ts new file mode 100644 index 000000000..a59c6bb17 --- /dev/null +++ b/test/specification-suite.test.ts @@ -0,0 +1,123 @@ +import { readFile } from 'node:fs/promises'; +import * as path from 'node:path'; +import Ajv2020, { type AnySchema } from 'ajv/dist/2020.js'; +import { describe, expect, it } from 'vitest'; + +const ROOT = path.resolve(__dirname, '..'); +const SPEC_ROOT = path.join(ROOT, 'docs', 'specification'); +const FIXTURE_ROOT = path.join(ROOT, 'test-fixtures', 'spec', 'suite-v0.1'); + +const normativeDocuments = [ + 'interoperability-conventions-v0.1.md', + 'team-routing-v0.1.md', + 'configuration-casting-v0.1.md', + 'governance-review-v0.1.md', + 'execution-preferences-v0.1.md', + 'planning-activation-v0.1.md', + 'ceremony-v0.1.md', + 'work-lifecycle-v0.1.md', + 'github-work-lifecycle-binding-v0.1.md', + 'coordination-handoff-v0.1.md', + 'automation-v0.1.md', +] as const; + +async function readJson(file: string): Promise { + return JSON.parse(await readFile(path.join(FIXTURE_ROOT, file), 'utf8')) as unknown; +} + +function createAjv(): Ajv2020 { + const ajv = new Ajv2020({ allErrors: true, strict: true }); + ajv.addFormat('uuid', /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i); + ajv.addFormat('date-time', value => !Number.isNaN(Date.parse(value))); + ajv.addFormat('uri-reference', value => !/\s/.test(value)); + return ajv; +} + +describe('Squad specification suite v0.1', () => { + it.each([ + ['index.schema.json', 'index.json'], + ['evidence.schema.json', 'governance-rejection.example.json'], + ])('validates %s fixtures', async (schemaFile, exampleFile) => { + const ajv = createAjv(); + const validate = ajv.compile(await readJson(schemaFile) as AnySchema); + const example = await readJson(exampleFile); + + expect(validate(example), JSON.stringify(validate.errors, null, 2)).toBe(true); + }); + + it('enforces suite index cross-document invariants', async () => { + const index = await readJson('index.json') as { + dialects: Array<{ id: string }>; + sharedSchemas: string[]; + profiles: Array<{ + id: string; + spec: string; + dialect: string; + dependencies: string[]; + capabilities: string[]; + manifest: string | null; + manifestSchema: string | null; + }>; + }; + const profileIds = new Set(index.profiles.map(profile => profile.id)); + const dialectIds = new Set(index.dialects.map(dialect => dialect.id)); + const caseIds = new Set(); + + expect(profileIds.size).toBe(index.profiles.length); + for (const profile of index.profiles) { + expect(dialectIds.has(profile.dialect), profile.id).toBe(true); + expect(profile.capabilities.every(capability => capability.startsWith(`${profile.id}#`)), profile.id).toBe(true); + expect(profile.dependencies.every(dependency => profileIds.has(dependency)), profile.id).toBe(true); + expect(await readFile(path.join(ROOT, profile.spec), 'utf8')).not.toHaveLength(0); + if (profile.manifest !== null) { + expect(profile.manifestSchema, profile.id).not.toBeNull(); + const manifest = JSON.parse(await readFile(path.join(ROOT, profile.manifest), 'utf8')) as { + profile?: string; + schema?: string; + cases?: Array<{ id: string }>; + }; + const schema = JSON.parse( + await readFile(path.join(ROOT, profile.manifestSchema!), 'utf8'), + ) as AnySchema; + const validate = createAjv().compile(schema); + + expect(validate(manifest), `${profile.id}: ${JSON.stringify(validate.errors, null, 2)}`).toBe(true); + expect(manifest.profile ?? manifest.schema, profile.id).toBe(profile.id); + for (const testCase of manifest.cases ?? []) { + const qualifiedId = `${profile.id}#${testCase.id}`; + expect(caseIds.has(qualifiedId), qualifiedId).toBe(false); + caseIds.add(qualifiedId); + } + } + } + + for (const schema of index.sharedSchemas) { + const document = JSON.parse(await readFile(path.join(ROOT, schema), 'utf8')) as AnySchema; + expect(() => createAjv().compile(document)).not.toThrow(); + } + }); + + it.each(normativeDocuments)('%s exposes falsifiable conformance sections', async (file) => { + const content = await readFile(path.join(SPEC_ROOT, file), 'utf8'); + + expect(content).toMatch(/\*\*Maturity:\*\*/); + expect(content).toMatch(/## \d+\. .*Conformance/i); + expect(content).toMatch(/\bMUST\b/); + expect(content).toMatch(/squad-interop\/v0\.1|Interoperability Conventions/); + }); + + it('keeps volatile provider catalogs and casting aesthetics non-normative', async () => { + const preferences = await readFile( + path.join(SPEC_ROOT, 'execution-preferences-v0.1.md'), + 'utf8', + ); + const casting = await readFile( + path.join(SPEC_ROOT, 'configuration-casting-v0.1.md'), + 'utf8', + ); + + expect(preferences).not.toMatch(/gpt-|claude-|gemini-/i); + expect(casting).toContain('Casting aesthetics are an'); + expect(casting).toContain('MUST NOT define authority'); + }); +}); From b1c07f6025485a4f896de636e6cc3aa4a0d552ce Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Fri, 25 Sep 2026 07:53:18 -0700 Subject: [PATCH 06/11] fix: revise specification suite publication claims Part of #2069 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> (cherry picked from commit f5ced9d051a727457df807f7ed49cb6271f9aef0) --- docs/specification/README.md | 226 ++++++++++------ docs/specification/artifact-inventory.md | 55 +++- docs/specification/automation-v0.1.md | 32 ++- docs/specification/ceremony-v0.1.md | 47 ++-- .../configuration-casting-v0.1.md | 94 ++++--- .../coordination-handoff-v0.1.md | 34 ++- .../execution-preferences-v0.1.md | 42 +-- .../github-work-lifecycle-binding-v0.1.md | 131 +++++++--- docs/specification/governance-review-v0.1.md | 51 ++-- .../interoperability-conventions-v0.1.md | 85 ++++-- .../specification/planning-activation-v0.1.md | 53 ++-- .../state-memory-requirements-v0.1.md | 50 ++-- docs/specification/team-routing-v0.1.md | 84 +++--- docs/specification/work-lifecycle-v0.1.md | 84 ++++-- package.json | 2 +- .../spec/suite-v0.1/evidence.schema.json | 244 +++++++++++++++++- .../governance-rejection.example.json | 58 +++-- test-fixtures/spec/suite-v0.1/index.json | 229 +++++++++++----- .../spec/suite-v0.1/index.schema.json | 105 +++++++- test/specification-suite.test.ts | 195 ++++++++++++-- 20 files changed, 1416 insertions(+), 485 deletions(-) diff --git a/docs/specification/README.md b/docs/specification/README.md index 7d5f3252c..5d856d1ef 100644 --- a/docs/specification/README.md +++ b/docs/specification/README.md @@ -6,91 +6,151 @@ **Draft date:** 2026-09-25 -**Working-draft provenance:** [PR #2074](https://github.com/bradygaster/squad/pull/2074), -whose revision lineage starts at immutable review baseline -[`ab80da18087b7d37755db2c3cc5880d194a10f52`](https://github.com/bradygaster/squad/commit/ab80da18087b7d37755db2c3cc5880d194a10f52). -Each subsequent draft snapshot is identified by its immutable Git commit in -that pull request; no publication tag has been assigned. - -**Document class:** Informative index; linked core/profile documents identify -their normative sections +**Working-draft provenance:** [PR #2078](https://github.com/bradygaster/squad/pull/2078) +contains this suite revision. Its rejected review baseline is +[`c03ba950fde071c8ec58a64504fdb7b5cf1206ef`](https://github.com/bradygaster/squad/commit/c03ba950fde071c8ec58a64504fdb7b5cf1206ef). +Revision cycle 2 is authored by `agent-orchestration-dev`; the original +Architect author and rejecting reviewers are excluded from implementation by +the governance lockout. + +The source index keeps `publication.revision` and `publication.digest` null +while it is a working draft. After review, the exact head SHA is recorded in +the PR body and review report. A publication pipeline may copy the source index +into a release artifact, set `publication.status` to `published`, insert that +already-existing reviewed head SHA, and compute the release artifact digest. +The derived release index is attached to the tag or release; it is not committed +back onto the same commit. This records an immutable revision in the suite +index without a self-referential hash that would invalidate the commit. + +**Document class:** Informative registry and navigation index + **Foundation profile:** [Squad Charter Profile v0.1](charter-v0.1.md) -This directory defines portable contracts for the on-disk artifacts that make -up a Squad. The specification is independent of the TypeScript reference -implementation. A producer or consumer can implement one artifact profile -without claiming that an entire Squad is conformant. - -## Documents - -- [Core v0.1](core-v0.1.md) defines shared terminology, conformance classes, - versions, paths, extensions, and trust requirements. -- [Interoperability Conventions v0.1](interoperability-conventions-v0.1.md) - defines suite discovery, stable authority identities, negotiation, shared - evidence, replay protection, and privacy/secrets exclusion. -- [Charter Profile v0.1](charter-v0.1.md) defines the first normative artifact - profile for `.squad/agents/{id}/charter.md`. -- [Team and Routing Profile v0.1](team-routing-v0.1.md) defines accountable - roster entries, capability ownership, and deterministic route selection. -- [Configuration and Casting Profile v0.1](configuration-casting-v0.1.md) - defines configuration precedence, cast provenance, and revision joins. -- [Governance and Review Profile v0.1](governance-review-v0.1.md) defines - reviewer independence, rejection lockout, reassignment, escalation, and - approval evidence. -- [Execution Preferences Profile v0.1](execution-preferences-v0.1.md) separates - portable execution intent from provider catalogs and runtime policy. -- [Planning and Activation Profile v0.1](planning-activation-v0.1.md) defines - PRD intake, decomposition, acceptance, and activation evidence. -- [Ceremony Profile v0.1](ceremony-v0.1.md) defines observable ceremony - declarations and completion records without standardizing meeting culture. -- [Work Lifecycle Profile v0.1](work-lifecycle-v0.1.md) defines the - provider-neutral issue-to-change lifecycle. -- [GitHub Work Lifecycle Binding v0.1](github-work-lifecycle-binding-v0.1.md) - maps the neutral lifecycle to GitHub issues, labels, branches, pull requests, - checks, reviews, merges, and closure. -- [Coordination and Handoff Profile v0.1](coordination-handoff-v0.1.md) defines - portable handoff envelopes and idempotent delivery semantics. -- [Automation Profile v0.1](automation-v0.1.md) defines schedule manifests, - execution claims, retry evidence, and provider boundaries. -- [State and Memory Requirements v0.1](state-memory-requirements-v0.1.md) is an - informative requirements draft with explicit promotion criteria. -- [Artifact inventory and coverage matrix](artifact-inventory.md) traces the - repository surface to its specification disposition and maturity. -- [`test-fixtures/spec/charter-v0.1/`](../../test-fixtures/spec/charter-v0.1/) - contains language-neutral positive, negative, legacy, and round-trip - fixtures. Its [`manifest.schema.json`](../../test-fixtures/spec/charter-v0.1/manifest.schema.json) - defines the portable manifest field contract. -- [`test-fixtures/spec/suite-v0.1/`](../../test-fixtures/spec/suite-v0.1/) - contains the `squad-conformance/v0.1` suite index, reusable evidence schema, - and language-neutral governance evidence example. The index registers the - existing Charter v0.1 manifest unchanged as a legacy dialect. - -## Dependency layers - -1. `squad-core/v0.1` defines common conformance, diagnostics, versioning, - extension, and trust rules. -2. `squad-interop/v0.1` defines discovery, negotiation, authority identity, - shared evidence, concurrency, replay protection, and secrets exclusion. -3. Artifact profiles define portable files and evidence independent of any - hosted provider. -4. Protocol profiles define state transitions and messages spanning artifacts. -5. Provider bindings map neutral protocol concepts to provider resources. -6. Informative requirements documents preserve conceptual coverage where - interoperability evidence is not mature enough for a truthful schema. - -Conformance is capability-specific. Supporting a GitHub binding does not imply -support for every artifact profile, and accepting one profile operation does -not imply editor, validator, executor, or provider capability. - -Normative references are maintained in -[Core v0.1 §2](core-v0.1.md#2-normative-references), including BCP 14, -RFC 3339, and CommonMark 0.31.2. +This directory maps all major stable or observable Squad surfaces. It +distinguishes the one executable normative profile from experimental schemas, +non-claimable requirements drafts, and informative coverage. + +## Maturity taxonomy + +The suite uses exactly four maturity values: + +| Maturity | Meaning | Claimable | +|---|---|---| +| `published-normative` | Executable manifest and schema with reviewed cases | Yes, only identifiers declared by that manifest | +| `experimental-schema` | Machine-valid schema or fixture, but no complete conformance manifest | No | +| `requirements-draft` | Conceptual and behavioral requirements awaiting portable fixtures | No | +| `informative-draft` | Coverage, observations, or volatile requirements | No | + +The document-class vocabulary is `normative-profile`, `normative-schema`, +`requirements`, and `informative`. The only conformance-class vocabulary is +`canonical`, `compatible`, `legacy`, and `runtime`. Implementation roles +(`producer`, `consumer`, `editor`, `validator`, and `executor`) describe what +software does; they are not claims. + +Capability identifiers use one grammar: +`squad-/v./`. The suite defines no `#operation` +alias. A registry entry without an executable manifest has no claimable +capabilities or conformance classes. + +## Document registry + +`Schema/manifest` uses `schema`, `manifest`, `fixture`, or `none`. Known +deviations are summarized here and expanded in the +[coverage matrix](artifact-inventory.md#known-unstable-and-non-conforming-behavior). + +| ID | Maturity / class | Dependencies | Claimable capabilities | Schema/manifest | Implementation status | Known deviations | Reader entry path | +|---|---|---|---|---|---|---|---| +| `squad-core/v0.1` | `requirements-draft` / requirements | None | None | None | Terminology is used by Charter | Canonical JSON and publication rules incomplete | [Core](core-v0.1.md), then Charter | +| `squad-interop/v0.1` | `experimental-schema` / normative schema | Core | None | Shared evidence schema; no manifest | Evidence validation is executable | No discovery or replay corpus | [Interoperability](interoperability-conventions-v0.1.md) | +| `squad-charter/v0.1` | `published-normative` / normative profile | Core | `squad-charter/v0.1/parse`, `squad-charter/v0.1/validate`, `squad-charter/v0.1/edit`, `squad-charter/v0.1/legacy-consume`, `squad-charter/v0.1/runtime` | Manifest and schema | Parser, validator, editor, compiler, and cases implemented | None registered | [Charter](charter-v0.1.md) | +| `squad-team-routing/v0.1` | `requirements-draft` / requirements | Core, Interop, Charter | None | None | Roster and routing exist in multiple shapes | Fallback and ambiguity differ | [Roster, then routing](team-routing-v0.1.md) | +| `squad-configuration-casting/v0.1` | `requirements-draft` / requirements | Core, Interop, Team/Routing | None | None | Configuration, registry, and history implemented | Legacy shapes and runtime-only keys | [Configuration, casting, provenance](configuration-casting-v0.1.md) | +| `squad-governance-review/v0.1` | `experimental-schema` / normative schema | Core, Interop, Team/Routing | None | Evidence schema plus rejection fixture; no manifest | Rejection records are machine-valid | Baseline owner self-revision conflict | [Evidence contract, then transitions](governance-review-v0.1.md) | +| `squad-execution-preferences/v0.1` | `requirements-draft` / requirements | Core, Interop, Charter, Configuration/Casting | None | None | Runtime selection exists | Provider catalogs and fallback are policy | [Execution preferences](execution-preferences-v0.1.md) | +| `squad-planning-activation/v0.1` | `requirements-draft` / requirements | Core, Interop, Team/Routing, Work Lifecycle | None | None | gh-aw planning records exist | Safe-output and limits are provider-specific | [Planning and activation](planning-activation-v0.1.md) | +| `squad-ceremony/v0.1` | `requirements-draft` / requirements | Core, Interop, Team/Routing | None | None | Builder preserves triggers and schedules | Event names, `auto`, and cooldown are unstable | [Ceremonies](ceremony-v0.1.md) | +| `squad-work-lifecycle/v0.1` | `requirements-draft` / requirements | Core, Interop, Team/Routing, Governance | None | None | Neutral state design only | No shared transition traces | [Neutral lifecycle](work-lifecycle-v0.1.md) | +| `squad-work-github/v0.1` | `requirements-draft` / requirements binding | Interop, Work Lifecycle, Governance | None | None | GitHub adapters exist | No deterministic provider transcripts | [GitHub binding](github-work-lifecycle-binding-v0.1.md) | +| `squad-coordination-handoff/v0.1` | `requirements-draft` / requirements | Core, Interop, Team/Routing | None | None | Session and handoff implementations exist | Ordering and recovery are provider-specific | [Coordination](coordination-handoff-v0.1.md) | +| `squad-automation/v0.1` | `requirements-draft` / requirements | Core, Interop, Coordination | None | None | Local and GitHub schedulers exist | Time, retry, and drift evidence differ | [Automation](automation-v0.1.md) | +| `squad-state-memory-requirements/v0.1` | `informative-draft` / informative | Core, Interop | None | None | Several state and memory providers exist | No common authority, recovery, or privacy corpus | [State authority, then memory classification](state-memory-requirements-v0.1.md) | +| `squad-portable-bundle/v1.0` | `informative-draft` / observed implementation surface | Core and Interop rules apply conceptually | None | Tested JSON shape; no published schema or manifest | `squad export` and `squad import` round-trip team, routing, decisions, casting, agents, and skills | Bundle `1.0` does not use Core profile grammar; histories, decisions, skills, and casting policy can contain sensitive data | [Coverage and promotion requirements](artifact-inventory.md#portable-export-and-import-bundle) | + +The machine-readable registry is +[`test-fixtures/spec/suite-v0.1/index.json`](../../test-fixtures/spec/suite-v0.1/index.json). +The governance JSON is registered as a fixture, not a conformance manifest. +Charter v0.1's manifest, schema, case IDs, and capability identifiers remain +unchanged. + +## Textual dependency adjacency and DAG + +Adjacency list: + +```text +squad-core/v0.1 -> [] +squad-interop/v0.1 -> [squad-core/v0.1] +squad-charter/v0.1 -> [squad-core/v0.1] +squad-team-routing/v0.1 -> [squad-core/v0.1, squad-interop/v0.1, squad-charter/v0.1] +squad-configuration-casting/v0.1 -> [squad-core/v0.1, squad-interop/v0.1, squad-team-routing/v0.1] +squad-governance-review/v0.1 -> [squad-core/v0.1, squad-interop/v0.1, squad-team-routing/v0.1] +squad-execution-preferences/v0.1 -> [squad-core/v0.1, squad-interop/v0.1, squad-charter/v0.1, squad-configuration-casting/v0.1] +squad-work-lifecycle/v0.1 -> [squad-core/v0.1, squad-interop/v0.1, squad-team-routing/v0.1, squad-governance-review/v0.1] +squad-work-github/v0.1 -> [squad-interop/v0.1, squad-work-lifecycle/v0.1, squad-governance-review/v0.1] +squad-planning-activation/v0.1 -> [squad-core/v0.1, squad-interop/v0.1, squad-team-routing/v0.1, squad-work-lifecycle/v0.1] +squad-ceremony/v0.1 -> [squad-core/v0.1, squad-interop/v0.1, squad-team-routing/v0.1] +squad-coordination-handoff/v0.1 -> [squad-core/v0.1, squad-interop/v0.1, squad-team-routing/v0.1] +squad-automation/v0.1 -> [squad-core/v0.1, squad-interop/v0.1, squad-coordination-handoff/v0.1] +squad-state-memory-requirements/v0.1 -> [squad-core/v0.1, squad-interop/v0.1] +squad-portable-bundle/v1.0 -> [conceptual Core and Interop constraints; no profile claim] +``` + +DAG reading order: + +```text +Core +├── Charter (only claimable profile) +├── Interop shared schema +│ ├── Team/Routing +│ │ ├── Configuration/Casting +│ │ │ └── Execution Preferences +│ │ ├── Governance +│ │ │ └── Work Lifecycle +│ │ │ ├── GitHub Binding +│ │ │ └── Planning/Activation +│ │ ├── Ceremony +│ │ └── Coordination +│ │ └── Automation +│ └── State/Memory Requirements +└── Portable Bundle observation (applies Core/Interop constraints without a claim) +``` + +There is no dependency cycle. Provider-neutral requirements precede the GitHub +binding. State authority and memory classification remain separate promotion +tracks. + +## External reference policy + +Material external assumptions use stable, versioned, section-level links and +are labeled normative or informative in the document that imports them. Core +[§2](core-v0.1.md#2-normative-references) imports BCP 14, RFC 3339, and +CommonMark 0.31.2. Interoperability +[§10](interoperability-conventions-v0.1.md#10-versioned-references-and-precedents) +and the GitHub binding +[§8](github-work-lifecycle-binding-v0.1.md#8-versioned-provider-assumptions-and-references) +identify their narrower assumptions. An informative precedent does not become +a suite requirement merely by being cited. + +The existing `test/docs-links.test.ts` command scans `docs/src/content/**` and +the root README, not `docs/specification/**`; there is no existing supported +link-check command for this directory. The suite test verifies every indexed +specification, schema, manifest, and fixture path exists. External URL +availability and section anchors remain review-time checks until the repository +link checker expands its supported scope. ## Versioning policy -The core and each artifact profile are versioned independently. Draft `0.x` -documents may change before publication, and unsupported versions fail closed. -After publication, a revision is immutable: editorial corrections are recorded -as dated errata, while normative changes require a new `vMAJOR.MINOR` -identifier. Patch components are not used. See -[Core v0.1 §10](core-v0.1.md#10-publication-errata-and-versioning). +Profile identifiers use `vMAJOR.MINOR`; mutable state revisions, provider API +versions, implementation versions, and the observed portable-bundle `1.0` +version are distinct. Draft documents may change before promotion. Published +normative revisions are immutable except for dated errata; behavior changes +require a new profile version. diff --git a/docs/specification/artifact-inventory.md b/docs/specification/artifact-inventory.md index 5d3b979b4..0696ece55 100644 --- a/docs/specification/artifact-inventory.md +++ b/docs/specification/artifact-inventory.md @@ -12,22 +12,24 @@ render them beneath `.squad/`. | Surface | Repository evidence | Disposition | Specification | Maturity or promotion rationale | |---|---|---|---|---| -| Discovery, negotiation, authority identity, shared evidence, secrets | Core/profile metadata, registry IDs, events, audit records | Stable normative dependency | Interoperability Conventions v0.1 | Cross-cutting mechanics must be shared before profile-specific conformance | +| Discovery, negotiation, authority identity, shared evidence, secrets | Core/profile metadata, registry IDs, events, audit records | Experimental shared schema; no claim | Interoperability Conventions v0.1 | Evidence schema exists; discovery and negotiation need a manifest | | Agent charter | `agents/{id}/charter.md`, compiler, validator, fixtures | Normative profile now | Charter v0.1 | Stable parser and round-trip evidence | -| Team roster | `team.md`, roster templates, presets | Normative profile now | Team and Routing v0.1 | Stable identity/accountability join; presentation variants remain compatible | -| Routing ownership | `routing.md`, routing parser, coordinator tests | Normative profile now | Team and Routing v0.1 | Deterministic table subset and fallback semantics are observable | -| Runtime configuration | `config.json`, config schema and loaders | Normative profile now | Configuration and Casting v0.1 | Portable intent standardized; implementation-only keys remain extensions | -| Casting provenance | `casting/registry.json`, `policy.json`, `history.json` | Normative profile now | Configuration and Casting v0.1 | Registry identity and revision join are stable; theme selection algorithm is informative | -| Governance and review | `governance.md`, reviewer protocol, PR requirements | Normative protocol now | Governance and Review v0.1 | Lockout, evidence, reassignment, escalation, and approval are externally auditable | -| Execution preferences | charters, config, model selector, lifecycle manager | Normative profile now | Execution Preferences v0.1 | Portable intent is stable; model catalogs and authorization remain runtime policy | -| PRD intake and planning | `prd-intake.md`, planning ontology, gh-aw workflows | Normative protocol now | Planning and Activation v0.1 | Artifact kinds, acceptance gates, and idempotency are observable | -| Ceremonies | `ceremonies.md`, ceremony reference, retro workflow | Normative artifact and record now | Ceremony v0.1 | Inputs, decisions, actions, roles, trigger, and cooldown are portable; meeting style is informative | -| Work lifecycle | issue lifecycle template, platform interfaces, worktree tests | Normative protocol now | Work Lifecycle v0.1 | Neutral states and evidence can be provider-independent | -| GitHub lifecycle | GitHub adapter, gh-aw workflows, issue/PR templates | Provider binding now | GitHub Work Lifecycle Binding v0.1 | GitHub resource mappings and failure semantics are observable | -| Handoffs and sessions | spawn reference, lifecycle manager, event buses | Normative envelope now | Coordination and Handoff v0.1 | Message identity and idempotency are portable; session transport internals are not | +| Team roster | `team.md`, roster templates, presets | Requirements draft; no claim | Team and Routing v0.1 | Promote independently with roster parsing, mutation, join, and diagnostic cases | +| Routing ownership | `routing.md`, routing parser, coordinator tests | Requirements draft; no claim | Team and Routing v0.1 | Promote independently with priority, fallback, ambiguity, stale-write, and resolution cases | +| Runtime configuration | `config.json`, config schema and loaders | Requirements draft; no claim | Configuration and Casting v0.1 | Promote independently with precedence and source-evidence cases | +| Casting identity | `casting/registry.json`, `policy.json` | Requirements draft; no claim | Configuration and Casting v0.1 | Promote independently with identity, recast, join, and migration cases | +| Casting provenance | `casting/history.json` | Requirements draft; no claim | Configuration and Casting v0.1 | Promote independently with append-only history, revision, replay, and CAS cases | +| Governance and review | `governance.md`, reviewer protocol, PR requirements | Experimental evidence schema; no claim | Governance and Review v0.1 | Add complete transition manifest beyond the representative rejection fixture | +| Execution preferences | charters, config, model selector, lifecycle manager | Requirements draft; no claim | Execution Preferences v0.1 | Add provider-neutral resolution cases and loss reporting | +| PRD intake and planning | `prd-intake.md`, planning ontology, gh-aw workflows | Requirements draft; no claim | Planning and Activation v0.1 | Add portable planning-record and activation manifest | +| Ceremonies | `ceremonies.md`, ceremony reference, retro workflow | Requirements draft; no claim | Ceremony v0.1 | Normalize event vocabulary and prove cooldown with injected clocks | +| Work lifecycle | issue lifecycle template, platform interfaces, worktree tests | Requirements draft; no claim | Work Lifecycle v0.1 | Run complete transition traces against two providers | +| GitHub lifecycle | GitHub adapter, gh-aw workflows, issue/PR templates | Requirements binding draft; no claim | GitHub Work Lifecycle Binding v0.1 | Add deterministic sanitized provider transcripts | +| Handoffs and sessions | spawn reference, lifecycle manager, event buses | Requirements draft; no claim | Coordination and Handoff v0.1 | Add portable envelope, ordering, expiry, and recovery cases | | State backends | state backend interfaces, worktree/orphan/two-layer implementations | Informative requirements now | State and Memory Requirements v0.1 | Authority and recovery vary by backend; promotion requires backend-neutral CAS fixtures | | Memory governance | memory classes, providers, audit records | Informative requirements now | State and Memory Requirements v0.1 | Classification is promising but provider naming and durable authority are not stable | -| Automation | schedule manifest, scheduler, local and GitHub providers | Normative profile now | Automation v0.1 | Manifest, claims, retry, and result evidence are stable | +| Automation | schedule manifest, scheduler, local and GitHub providers | Requirements draft; no claim | Automation v0.1 | Add portable trigger, clock, retry, drift, and result cases | +| Portable export/import | `squad export`, `squad import`, CLI round-trip tests | Informative observed surface; no claim | `squad-portable-bundle/v1.0` coverage below | Tested implementation exists without a published schema or conformance manifest | | Skills | `skills/{name}/SKILL.md`, loader and discovery | Deferred | Future Skills Profile | Promote after metadata precedence, imports, script trust, and compatibility fixtures stabilize | | Coordinator prompts and UI wording | templates, generated agent prompt, shell | Informative guidance | Implementation documentation | Prompts and wording are implementation inputs, not interoperability contracts | | Telemetry and cost accounting | OpenTelemetry and cost modules | Deferred | Future Observability Binding | Promote after common event identity and privacy/redaction contracts stabilize | @@ -89,7 +91,7 @@ is equally mature. The next promotion sequence is: - Current GitHub adapters rely on installed `gh` behavior and do not yet expose API-version negotiation evidence required by the GitHub binding. - Casting universe lists and persona names are volatile display metadata and - MUST NOT be used as authority identity. + must not be used as authority identity. - Publication requires an independent reviewer for each normative profile and binding; this working draft supplies reviewable contracts, not publication. - The reusable conformance dialect still needs shared document-case, @@ -100,3 +102,28 @@ is equally mature. The next promotion sequence is: ordering, provider transcript sanitization, partial-failure taxonomy, pass/fail/skip semantics, required extensions, and deprecation windows remain explicit publication blockers. +- The portable export/import bundle uses implementation version `1.0`, not the + Core `vMAJOR.MINOR` profile grammar, and has no published schema. Its histories, + decisions, skills, registry, and policy content require explicit privacy and + secrets review before sharing. + +## Portable export and import bundle + +The tested CLI surface exports and imports `team.md`, `routing.md`, +`decisions.md`, casting registry/history/policy, agent charters and histories, +and skills. Local files and GitHub repository transport are implemented. + +This surface is informative coverage under the provisional identifier +`squad-portable-bundle/v1.0`; it is not a profile or capability claim. Core +path-containment and version-separation rules apply conceptually. Interop +privacy rules also apply: credentials, authorization headers, hidden prompts, +secret environment values, private tool output, and unrelated personal data +do not belong in a portable bundle. Current CLI warnings tell users to review +histories, decisions, and team content, but the bundle has no field-level +redaction declaration. + +Promotion requires a language-neutral schema, explicit required and optional +fields, canonical serialization and digest rules, compatibility fixtures for +legacy `team`/`decisions` aliases, import conflict semantics, version +negotiation, secret and privacy mutation cases, and round trips that cover +local and repository transport without provider credentials in evidence. diff --git a/docs/specification/automation-v0.1.md b/docs/specification/automation-v0.1.md index f3f069722..0e4ca6ee9 100644 --- a/docs/specification/automation-v0.1.md +++ b/docs/specification/automation-v0.1.md @@ -2,11 +2,19 @@ **Status:** Working Draft -**Document class:** Normative unless marked informative +**Document class:** Non-normative requirements draft **Profile identifier:** `squad-automation/v0.1` -**Maturity:** Experimental normative +**Maturity:** Requirements draft; non-claimable + +**Claimable capabilities:** None + +**Implementation status:** Local and GitHub scheduling implementations exist, +but there is no portable execution manifest. + +**Known deviations:** Time-zone behavior, retry evidence, generated-provider +drift, and result records differ by provider. **Depends on:** `squad-core/v0.1`, `squad-interop/v0.1`, `squad-coordination-handoff/v0.1` @@ -28,7 +36,7 @@ concurrency key. Trigger types are `cron`, `interval`, `event`, and `startup`. Task classes are `workflow`, `script`, `agent`, and `webhook`; bindings map class names to implementation operations. -Task references are inert data. Consumers MUST NOT execute a task until +Task references are inert data. Consumers must not execute a task until authorization and provider policy checks succeed. ## 3. Execution states @@ -41,13 +49,13 @@ Run and attempt records use the shared evidence envelope. ## 4. Trigger and time semantics -Cron bindings MUST declare time zone and daylight-saving behavior. Interval +Cron bindings must declare time zone and daylight-saving behavior. Interval triggers use elapsed duration, not calendar arithmetic. Event triggers require a stable event ID. Startup triggers are idempotent per executor startup ID. -## 5. Capabilities and roles +## 5. Candidate operations and roles -Capabilities are `automation.consume`, `automation.evaluate`, +Candidate operations are `automation.consume`, `automation.evaluate`, `automation.claim`, `automation.execute`, and `automation.generate-provider`. Evaluators decide eligibility; executors claim and run; generators emit provider-native configuration but do not become manifest authority. @@ -57,7 +65,7 @@ provider-native configuration but do not become manifest authority. Invalid triggers, unknown providers, denied tasks, claim conflict, timeout, spawn failure, nonzero result, generation drift, and exhausted retries are distinct failures. Claims use compare-and-swap. A run key deduplicates retries -and multi-provider delivery. Provider generation MUST be reproducible from the +and multi-provider delivery. Provider generation must be reproducible from the same manifest and generator version. ## 7. Versioning and extensions @@ -69,12 +77,12 @@ closed. ## 8. Security and privacy Script and webhook execution cross a trust boundary and require explicit -authorization. Arguments MUST remain vectors rather than shell-concatenated -strings. Logs and results MUST redact secrets and SHOULD store large output by +authorization. Arguments must remain vectors rather than shell-concatenated +strings. Logs and results must redact secrets and should store large output by reference. -## 9. Conformance +## 9. Promotion criteria -Tests MUST cover every trigger, time zone declaration, duplicate event, -startup deduplication, claim race, retry and backoff, timeout, provider +Publish a manifest covering every trigger, time-zone declaration, duplicate +event, startup deduplication, claim race, retry and backoff, timeout, provider generation drift, denied execution, and secret-safe diagnostics. diff --git a/docs/specification/ceremony-v0.1.md b/docs/specification/ceremony-v0.1.md index 9a4c6fb77..02da77714 100644 --- a/docs/specification/ceremony-v0.1.md +++ b/docs/specification/ceremony-v0.1.md @@ -2,11 +2,20 @@ **Status:** Working Draft -**Document class:** Normative unless marked informative +**Document class:** Non-normative requirements draft **Profile identifier:** `squad-ceremony/v0.1` -**Maturity:** Experimental normative +**Maturity:** Requirements draft; non-claimable + +**Claimable capabilities:** None + +**Implementation status:** The SDK builder preserves arbitrary trigger strings +and schedule text. Existing tests demonstrate `manual`, `schedule`, and an +event name (`pr-merged`); they do not prove runtime cooldown enforcement. + +**Known deviations:** Legacy `auto` is ambiguous, event names are not +normalized, and cooldown currently has schedule-cadence evidence only. **Depends on:** `squad-core/v0.1`, `squad-interop/v0.1`, `squad-team-routing/v0.1` @@ -24,12 +33,15 @@ style, consensus, or synchronous human attendance. A ceremony declares ID, enabled state, trigger mode, timing, condition reference, facilitator selector, participant selectors, cooldown policy, input -artifact kinds, and output requirements. Trigger modes are `manual`, `event`, -and `schedule`. Natural-language conditions MAY be preserved but are +record kinds, and output requirements. Canonical trigger modes are `manual`, +`schedule`, and `event:`. Existing bare event names such as `pr-merged` +map to `event:pr-merged`. Legacy `auto` is accepted only as ambiguous compatible +input and requires an implementation mapping to either a named event or a +schedule. Natural-language conditions may be preserved but are noncanonical unless paired with a namespaced deterministic evaluator. Participant selectors are `accountable-owner`, `all-involved`, -`all-relevant`, or explicit member IDs. Resolution MUST produce the exact +`all-relevant`, or explicit member IDs. Resolution must produce the exact participant set used. ## 3. Execution record @@ -44,13 +56,13 @@ owners, and dispositions. `eligible -> claimed -> running -> completed|failed|cancelled` Cooldown begins at terminal completion. A run during cooldown is `suppressed` -with evidence and MUST NOT create a second active claim. Failure policy is +with evidence and must not create a second active claim. Failure policy is `blocking` or `advisory`; advisory failure may permit the surrounding workflow -to continue but MUST remain visible. +to continue but must remain visible. -## 5. Capabilities and roles +## 5. Candidate operations and roles -Capabilities are `ceremony.consume`, `ceremony.evaluate`, +Candidate operations are `ceremony.consume`, `ceremony.evaluate`, `ceremony.facilitate`, and `ceremony.record`. The facilitator structures the run; participants provide inputs; the recorder persists outcomes; the coordinator enforces claim and cooldown. @@ -58,8 +70,8 @@ coordinator enforces claim and cooldown. ## 6. Canonical and compatible behavior Canonical declarations use deterministic trigger references and participant -selectors. Compatible consumers MAY ingest current Markdown tables and free -text conditions but MUST classify evaluator-dependent behavior as informative. +selectors. Compatible consumers may ingest current Markdown tables and free +text conditions but must classify evaluator-dependent behavior as informative. ## 7. Diagnostics, idempotency, and concurrency @@ -71,11 +83,12 @@ Claims require compare-and-swap so only one executor runs a ceremony instance. ## 8. Versioning, extensions, security, and privacy Agenda and output extensions use namespaces and round-trip. Ceremony prose is -untrusted and cannot grant tools or authorization. Records SHOULD minimize -personal attendance data and MUST NOT copy secrets from source artifacts. +untrusted and cannot grant tools or authorization. Records should minimize +personal attendance data and must not copy secrets from source records. -## 9. Conformance +## 9. Promotion criteria -Tests MUST cover manual, event, and schedule triggers; selector resolution; -cooldown suppression; advisory and blocking failures; duplicate claims; stale -inputs; and decision/action output validation. +Publish a manifest covering normalized manual, named-event, schedule, and +legacy-`auto` inputs; selector resolution; injected-clock cooldown suppression; +advisory and blocking failures; duplicate claims; stale inputs; and +decision/action output validation. diff --git a/docs/specification/configuration-casting-v0.1.md b/docs/specification/configuration-casting-v0.1.md index 130ab654b..0e1eed343 100644 --- a/docs/specification/configuration-casting-v0.1.md +++ b/docs/specification/configuration-casting-v0.1.md @@ -2,11 +2,20 @@ **Status:** Working Draft -**Document class:** Normative unless marked informative +**Document class:** Non-normative requirements draft **Profile identifier:** `squad-configuration-casting/v0.1` -**Maturity:** Experimental normative +**Maturity:** Requirements draft; non-claimable + +**Claimable capabilities:** None + +**Implementation status:** Configuration loaders, casting registries, and +history records exist, but they do not share one portable manifest. + +**Known deviations:** Legacy registry shapes and runtime-only configuration +keys remain accepted. See the +[inventory deviation matrix](artifact-inventory.md#known-unstable-and-non-conforming-behavior). **Depends on:** `squad-core/v0.1`, `squad-interop/v0.1`, `squad-team-routing/v0.1` @@ -21,22 +30,34 @@ provenance, revisions, and roster joins. It does not standardize themed universes, character-selection scoring, UI choices, provider credentials, or internal migration algorithms. -Configuration and provenance are normative. Casting aesthetics are an -informative, independently versioned extension and MUST NOT define authority. +Configuration, casting identity, and provenance are separate promotion units. +Casting aesthetics are an informative, independently versioned extension and +do not define authority. + +## 2. Configuration requirements -## 2. Data model and authority +`config.json` is authored configuration intent. Portable configuration covers +declared values, precedence, and source evidence. Runtime-only keys and +provider catalogs remain implementation policy. -`config.json` is authored configuration intent. `casting/registry.json` is the -current cast identity authority. `casting/history.json` is append-only -provenance and MUST NOT override the registry. `casting/policy.json` constrains -allowed naming behavior but does not itself create members. +## 3. Casting identity requirements -Every registry document MUST declare a schema identifier, schema version, +`casting/registry.json` is the current cast identity authority. +`casting/policy.json` constrains allowed naming behavior but does not itself +create members. + +Every registry document must declare a schema identifier, schema version, monotonic revision, generation timestamp, and agent map keyed by member ID. -Each agent entry MUST include display name, persistent name, accountable role, +Each agent entry must include display name, persistent name, accountable role, universe or naming source, status, creation time, and update time. -## 3. Precedence and joins +## 4. Provenance requirements + +`casting/history.json` is append-only provenance and does not override the +registry. Each history entry records the operation, prior and result revision, +actor, time, and identity joins needed to reproduce the change. + +## 5. Precedence and joins Portable configuration precedence is: @@ -46,55 +67,58 @@ Portable configuration precedence is: 4. charter preference; 5. runtime automatic policy. -Lower layers MUST NOT overwrite higher-layer authored intent. A value of +Lower layers must not overwrite higher-layer authored intent. A value of `auto` requests runtime selection and is not a concrete override. -Registry IDs MUST join to active team IDs. Recasting MAY change display names -but MUST preserve member IDs unless an explicit identity migration is recorded. +Registry IDs must join to active team IDs. Recasting may change display names +but must preserve member IDs unless an explicit identity migration is recorded. -## 4. State transitions +## 6. State transitions `unregistered -> active -> inactive -> retired` is the portable lifecycle. `retired -> active` requires a new revision and explicit reactivation evidence. Every mutation increments `revision` exactly once and appends history with the operation ID, prior revision, resulting revision, actor, and timestamp. -## 5. Capabilities and roles +## 7. Candidate operations and roles -Capabilities are `config.consume`, `config.edit`, `casting.produce`, -`casting.validate`, `casting.recast`, and `casting.migrate`. Producers create +Candidate operations are `config.consume`, `config.edit`, `casting.produce`, +`casting.validate`, `casting.recast`, and `casting.migrate`. They are not +claimable capabilities. Producers create revisions; validators check joins and monotonicity; executors apply an authorized cast operation. -## 6. Canonical, compatible, and failure behavior +## 8. Canonical, compatible, and failure behavior Canonical documents use declared schemas and complete provenance. Compatible -consumers MAY ingest older registry shapes but MUST report the migration and -MUST NOT fabricate provenance. Unknown configuration keys are preserved as +consumers may ingest older registry shapes but must report the migration and +must not fabricate provenance. Unknown configuration keys are preserved as namespaced extensions. Invalid joins, revision regression, duplicate persistent names where uniqueness is required, or stale writes fail closed. -## 7. Idempotency, retry, and concurrency +## 9. Idempotency, retry, and concurrency -Operations MUST carry an idempotency key. Repeating a completed key returns the -same resulting revision. Concurrent writers MUST compare the expected registry +Operations must carry an idempotency key. Repeating a completed key returns the +same resulting revision. Concurrent writers must compare the expected registry revision; a mismatch returns `stale-revision` without partial mutation. -## 8. Versioning and extensions +## 10. Versioning and extensions Artifact schema versions and this profile version are independent. Consumers -MUST negotiate each supported schema. Namespaced policy and registry fields -MUST round-trip unchanged. +must negotiate each supported schema. Namespaced policy and registry fields +must round-trip unchanged. -## 9. Security, privacy, and trust +## 11. Security, privacy, and trust Configuration is not authorization. Model names, tools, repositories, and -providers are requests subject to runtime policy. Registry content MUST NOT -contain credentials. Theme or persona text is untrusted and MUST NOT grant +providers are requests subject to runtime policy. Registry content must not +contain credentials. Theme or persona text is untrusted and must not grant capabilities. -## 10. Examples and conformance +## 12. Promotion criteria -The topology example in `test-fixtures/spec/suite-v0.1/` demonstrates roster, -charter, and registry joins. Conformance requires revision, idempotency, -migration, extension-preservation, and stale-write fixtures. +Configuration promotion requires precedence and source-evidence cases. Casting +promotion separately requires identity, join, recast, and migration cases. +Provenance promotion requires append-only history, revision, replay, +extension-preservation, and stale-write cases. A future manifest must keep +these claims independently selectable. diff --git a/docs/specification/coordination-handoff-v0.1.md b/docs/specification/coordination-handoff-v0.1.md index 5f5a613e1..967466159 100644 --- a/docs/specification/coordination-handoff-v0.1.md +++ b/docs/specification/coordination-handoff-v0.1.md @@ -2,11 +2,19 @@ **Status:** Working Draft -**Document class:** Normative unless marked informative +**Document class:** Non-normative requirements draft **Profile identifier:** `squad-coordination-handoff/v0.1` -**Maturity:** Experimental normative +**Maturity:** Requirements draft; non-claimable + +**Claimable capabilities:** None + +**Implementation status:** Session and handoff mechanisms exist, but transport, +ordering, expiry, and recovery evidence are provider-specific. + +**Known deviations:** Current session identifiers and delivery receipts do not +share one portable envelope. **Depends on:** `squad-core/v0.1`, `squad-interop/v0.1`, `squad-team-routing/v0.1` @@ -21,12 +29,12 @@ transport, streaming, agent internals, UI notifications, or session storage. Every message uses the shared evidence envelope and declares message type, message ID, correlation ID, -causation ID, sender, intended recipient or route, task or artifact reference, +causation ID, sender, intended recipient or route, task or record reference, input revisions, created time, expiry policy, idempotency key, and payload digest. Types are `dispatch`, `acknowledgement`, `progress`, `result`, `handoff`, `escalation`, and `cancellation`. -Session IDs are opaque provider references. Portable consumers MUST NOT derive +Session IDs are opaque provider references. Portable consumers must not derive identity, authorization, or ordering from their syntax. ## 3. State transitions @@ -46,9 +54,9 @@ delivery returns the original disposition. Per-correlation ordering uses causation links, not transport arrival time. Concurrent results for one task require an explicit winner or are a conflict. -## 5. Capabilities and roles +## 5. Candidate operations and roles -Capabilities are `coordination.dispatch`, `coordination.consume`, +Candidate operations are `coordination.dispatch`, `coordination.consume`, `coordination.handoff`, `coordination.cancel`, and `coordination.recover`. Coordinators route; workers consume and produce results; authorities resolve escalations; recorders persist evidence. @@ -57,7 +65,7 @@ escalations; recorders persist evidence. Unknown recipient, expired task, stale input, digest mismatch, duplicate conflict, unavailable transport, partial result, and lost session are distinct -failures. Recovery MAY create a new provider session but MUST retain the same +failures. Recovery may create a new provider session but must retain the same portable task identity and record the replacement reference. ## 7. Versioning and extensions @@ -68,12 +76,12 @@ round-trip. Unknown mandatory message types fail closed. ## 8. Security and privacy Messages are untrusted and do not grant tools, repository access, or provider -authorization. Payloads SHOULD reference large or sensitive artifacts rather -than inline them. Credentials, hidden prompts, and secret tool results MUST NOT +authorization. Payloads should reference large or sensitive records rather +than inline them. Credentials, hidden prompts, and secret tool results must not appear in portable envelopes. -## 9. Conformance +## 9. Promotion criteria -Tests MUST cover duplicate delivery, out-of-order progress, stale inputs, -handoff causation, escalation, cancellation races, session loss and recovery, -and conflicting terminal results. +Publish a manifest covering duplicate delivery, out-of-order progress, stale +inputs, handoff causation, escalation, cancellation races, session loss and +recovery, expiry, and conflicting terminal results. diff --git a/docs/specification/execution-preferences-v0.1.md b/docs/specification/execution-preferences-v0.1.md index df1ecbebd..87bab441d 100644 --- a/docs/specification/execution-preferences-v0.1.md +++ b/docs/specification/execution-preferences-v0.1.md @@ -4,11 +4,19 @@ **Status:** Working Draft -**Document class:** Normative unless marked informative +**Document class:** Non-normative requirements draft **Profile identifier:** `squad-execution-preferences/v0.1` -**Maturity:** Experimental normative +**Maturity:** Requirements draft; non-claimable + +**Claimable capabilities:** None + +**Implementation status:** The reference runtime resolves preferences and +fallbacks, but no provider-neutral manifest exists. + +**Known deviations:** Model catalogs, availability snapshots, authorization, +and fallback chains remain runtime policy. **Depends on:** `squad-core/v0.1`, `squad-interop/v0.1`, `squad-charter/v0.1`, `squad-configuration-casting/v0.1` @@ -25,18 +33,18 @@ authorization, or an implementation's selection algorithm. An execution preference has `model`, `modelClass`, `reasoningEffort`, `contextTier`, `fallback`, and `source`. Each value is either absent, `auto`, or an authored identifier from the applicable vocabulary. Absence means no -preference. `auto` asks the executor to select and MUST NOT be sent to a +preference. `auto` asks the executor to select and must not be sent to a provider as a literal model or tier. Portable reasoning intent is `low`, `medium`, `high`, `xhigh`, or `max`. -Portable context intent is `default` or `long_context`. Executors MAY support a -subset but MUST make unsupported values observable. +Portable context intent is `default` or `long_context`. Executors may support a +subset but must make unsupported values observable. ## 3. Precedence The order is explicit operation input, session directive, repository configuration, charter preference, task-aware automatic policy, then platform -default. A higher layer MAY set `auto`, which delegates selection to lower +default. A higher layer may set `auto`, which delegates selection to lower runtime policy without erasing the source evidence. ## 4. Resolution result and states @@ -48,22 +56,22 @@ Resolution produces `requested`, `selected`, `source`, `fallbackAttempt`, Availability means the runtime can address a model. Authorization means the actor may use it. Policy disposition means local implementation policy permits -it. These checks are independent and MUST NOT be collapsed into one silent +it. These checks are independent and must not be collapsed into one silent fallback. ## 5. Runtime fallback behavior -Fallback MAY be `none`, `same-provider`, `same-class`, or `platform-default`. -An executor MUST NOT broaden beyond the authored constraint. Every attempt MUST +Fallback may be `none`, `same-provider`, `same-class`, or `platform-default`. +An executor must not broaden beyond the authored constraint. Every attempt must be recorded. After the configured attempt limit, execution fails explicitly. Provider-specific chains and current model names are informative implementation policy, not part of this profile. -## 6. Capabilities and behavior classes +## 6. Candidate operations and behavior classes -Capabilities are `preferences.consume`, `preferences.resolve`, +Candidate operations are `preferences.consume`, `preferences.resolve`, `preferences.explain`, and `preferences.execute`. Canonical executors preserve -source and disposition evidence. Compatible executors MAY map unsupported +source and disposition evidence. Compatible executors may map unsupported reasoning or context values to provider vocabulary only when the mapping is declared and loss is reported. @@ -84,11 +92,11 @@ semantics fail closed. ## 9. Security and privacy Preferences never grant provider access, tools, data access, or spending -authority. Diagnostics MUST NOT expose credentials, account entitlements, or +authority. Diagnostics must not expose credentials, account entitlements, or private policy text. -## 10. Conformance +## 10. Promotion criteria -Tests MUST cover absence versus `auto`, each precedence layer, unavailable and -unauthorized models, policy denial, constrained fallback, exhausted retries, -and reasoning/context subset mapping. +Publish a manifest covering absence versus `auto`, each precedence layer, +unavailable and unauthorized selections, policy denial, constrained fallback, +exhausted retries, snapshot identity, and lossy reasoning/context mappings. diff --git a/docs/specification/github-work-lifecycle-binding-v0.1.md b/docs/specification/github-work-lifecycle-binding-v0.1.md index 1b9f2444a..50a5ff676 100644 --- a/docs/specification/github-work-lifecycle-binding-v0.1.md +++ b/docs/specification/github-work-lifecycle-binding-v0.1.md @@ -2,11 +2,20 @@ **Status:** Working Draft -**Document class:** Normative unless marked informative +**Document class:** Non-normative requirements draft **Binding identifier:** `squad-work-github/v0.1` -**Maturity:** Experimental normative provider binding +**Maturity:** Requirements draft; non-claimable + +**Claimable capabilities:** None + +**Implementation status:** GitHub adapters exist, but the repository has no +versioned sanitized transcript corpus for deterministic provider mapping. + +**Known deviations:** API-version evidence, composite state, CAS tokens, +multi-label handling, partial failure, and drift records are not emitted +uniformly. **Depends on:** `squad-interop/v0.1`, `squad-work-lifecycle/v0.1`, `squad-governance-review/v0.1` @@ -32,75 +41,131 @@ REST versus CLI transport, repository merge policy, or organization rulesets. ## 3. Labels and references -The binding reserves `squad` and `squad:{member-id}`. Implementations MUST +The binding reserves `squad` and `squad:{member-id}`. Implementations must ensure required labels exist before relying on them. Issue, pull request, -commit, branch, and check references MUST include repository identity. +commit, branch, and check references must include repository identity. -Temporary safe-output references MAY be used during one transaction but MUST +Temporary safe-output references may be used during one transaction but must resolve before the operation is considered successful. -## 4. State mapping - -Open without accountable label is `untriaged`; labeled without workspace is -`assigned`; claimed branch is `in-progress`; open pull request is -`needs-review`; `CHANGES_REQUESTED` is `changes-requested`; failing required -checks is `checks-failed`; current-revision approval plus passing required -checks is `ready`; merged is `integrated`; issue closure after merge is `done`. - -Draft pull requests remain `in-progress` unless an extension declares them -reviewable. A closed unmerged pull request is not integrated. +## 4. State mapping precedence and composition + +Bindings evaluate one provider snapshot and retain every observed condition, +then select one portable state by this precedence: + +1. merged change -> `integrated`, or `done` when the linked issue has an + explicit post-merge closure disposition; +2. authorized cancellation or closed unmerged change -> `cancelled`; +3. active dependency, authority, merge-conflict, or provider blocker -> + `blocked`; +4. current-revision blocking review -> `changes-requested`; +5. failed or cancelled required check -> `checks-failed`; +6. current-revision independent approval plus passing required checks -> + `ready`; +7. non-draft open pull request -> `needs-review`; +8. draft pull request or claimed workspace -> `in-progress`; +9. exactly one accountable owner label -> `assigned`; +10. otherwise -> `untriaged`. + +Thus an open pull request with changes requested, failing checks, and a +dependency blocker maps to `blocked`, while the review and check conditions +remain in evidence for deterministic recovery. Clearing the blocker recomputes +the snapshot and yields `changes-requested` before `checks-failed`. + +Multiple distinct `squad:{member-id}` labels are an ownership conflict, not +first-label-wins behavior. Duplicate copies of the same label collapse to one +condition. Draft pull requests remain `in-progress`. A closed unmerged pull +request is `cancelled`, never `integrated`. ## 5. Review and merge authority GitHub review state alone does not prove reviewer independence or lockout; the governance evidence record supplies that proof. Branch protection, rulesets, -actor permissions, and merge queue policy remain GitHub authority and MUST be +actor permissions, and merge queue policy remain GitHub authority and must be checked at execution time. -## 6. Idempotency and concurrency +## 6. Revision tokens, idempotency, and stale writes -Mutation keys SHOULD be stored in issue or pull request evidence. Creation -retries MUST search exact recorded mappings before creating another resource. -Updates use current issue or pull request revision evidence when available. -Merge retries MUST recognize an already merged pull request as the original -successful result. +The portable observation token is the digest of a canonical provider snapshot +containing repository node ID, issue node ID and `updated_at`, pull-request node +ID and `updated_at`, head SHA, base SHA, review-decision snapshot, required-check +snapshot, owner-label set, dependency-blocker set, and ruleset observation. +Transport ETags may optimize reads but do not replace this token. -## 7. Failure semantics +Every mutation supplies the expected snapshot digest and an idempotency key. +Immediately before mutation, the binding re-reads all fields used by the +operation. A digest mismatch returns `conflict` with the fresh snapshot and no +further mutation. A stale head SHA specifically returns `stale-revision`. +Creation retries search the exact recorded mapping before creating a resource. +Merge retries recognize an already merged pull request as the original result. +The same idempotency key with a different request digest is invalid. + +## 7. Rate limits, partial failure, and provider drift Missing labels, inaccessible repositories, rate limits, secondary limits, ruleset denial, stale head SHA, merge conflicts, unresolved temporary references, partial issue creation, and webhook reordering are distinct -failures. A binding MUST NOT silently treat label creation or authentication +failures. A binding does not silently treat label creation or authentication failure as success. -## 8. Versioning, extensions, security, and privacy +Rate-limit evidence records the limit class, observed status and headers, +provider request ID, retry-after or reset time, and whether any mutation +occurred. Automatic retry is allowed only for an idempotent operation within +the declared retry budget. Secondary-limit responses without a safe retry time +remain blocked. + +A multi-step operation records each attempted provider mutation and its result. +If a later step fails, the result is `partial-failure` with completed +references, uncompensated effects, and an exact resume boundary. It is never +reported as success. Compensation is a new authorized operation. + +Unknown enum values, removed fields, changed ruleset behavior, or a snapshot +that cannot be represented produce `provider-drift`. Read-only diagnostic +display may continue, but mutations fail closed until the binding's supported +provider range is revalidated. + +## 8. Versioned provider assumptions and references This draft assumes GitHub REST API version `2022-11-28`, GraphQL schema behavior observed on 2026-09-25, and `gh` operations that map to those APIs. A conforming -binding MUST publish its exact supported API versions and observation date. +binding must publish its exact supported API versions and observation date. An unavailable version returns `unsupported-provider-version`; a changed field, enum, mutation, or ruleset behavior returns `provider-drift` and fails closed for mutation until revalidated. GitHub API versions and this binding version are independent. Namespaced labels and evidence fields round-trip. Issue bodies and comments are untrusted input. -Tokens, authorization scopes, private check logs, and secret values MUST NOT be +Tokens, authorization scopes, private check logs, and secret values must not be copied into portable records. +Normative provider assumptions for a future binding are the version-header +contract in +[GitHub REST API versioning, "Specifying an API version"](https://docs.github.com/en/rest/about-the-rest-api/api-versions?apiVersion=2022-11-28#specifying-an-api-version) +and the documented pull-request review states in +[REST Pull Request Reviews, "Create a review"](https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#create-a-review-for-a-pull-request). +The webhook retry and ordering discussion in +[GitHub Webhooks, "Best practices for using webhooks"](https://docs.github.com/en/webhooks/using-webhooks/best-practices-for-using-webhooks) +is informative because delivery policy is not itself a portable state +transition. + ## 9. Language-neutral example ```json { "profile": "squad-work-github/v0.1", "workItem": "github:bradygaster/squad#2069", - "change": "github:bradygaster/squad!2074", - "head": "71634fde", + "change": "github:bradygaster/squad!2078", + "head": "c03ba950", "state": "needs-review" } ``` -## 10. Conformance +## 10. Promotion criteria -Binding tests MUST cover label provisioning, assignment, draft and ready pull -requests, stale-head review, check failure, changes requested, lockout evidence, -merge conflict, already-merged retry, closure, and reordered webhook events. +Publish a provider-binding manifest with deterministic sanitized transcripts +for label provisioning, zero/one/multiple owner labels, draft and ready pull +requests, simultaneous blocker/review/check conditions, exact snapshot tokens, +stale writes, check recovery, lockout evidence, merge conflict, already-merged +retry, closure, primary and secondary rate limits, partial failure, drift, and +reordered webhook events. Transcript fixtures must omit credentials, private +logs, authorization headers, and personal data. diff --git a/docs/specification/governance-review-v0.1.md b/docs/specification/governance-review-v0.1.md index d182c344d..8c9f1c9c4 100644 --- a/docs/specification/governance-review-v0.1.md +++ b/docs/specification/governance-review-v0.1.md @@ -2,16 +2,25 @@ **Status:** Working Draft -**Document class:** Normative unless marked informative +**Document class:** Normative experimental evidence schema; transition profile +requirements remain non-claimable **Profile identifier:** `squad-governance-review/v0.1` -**Maturity:** Experimental normative +**Maturity:** Experimental schema; non-claimable + +**Claimable capabilities:** None + +**Implementation status:** The shared evidence schema validates representative +governance transitions. No executable transition manifest is registered. + +**Known deviations:** The rejected baseline allowed owner self-revision in +repository governance and lacked one machine-readable transition contract. **Depends on:** `squad-core/v0.1`, `squad-interop/v0.1`, `squad-team-routing/v0.1` -**Protocol artifact:** governance evidence records +**Protocol record:** governance evidence records ## 1. Scope and non-goals @@ -23,18 +32,20 @@ feedback wording, repository merge policy, or who is authorized by a provider. ## 2. Roles and vocabulary The **accountable owner** owns the requirement and final handoff. The -**artifact author** produces a revision. A **blocking reviewer** issues +**revision author** produces a reviewed-subject revision. A **blocking reviewer** issues verdicts. A **revision author** independently produces the next revision after rejection. A **coordinator** validates transitions. An **authority** resolves deadlock or policy disputes. ## 3. Evidence record -Every transition MUST use the shared evidence envelope conforming to +Every represented transition MUST use the shared evidence envelope conforming to `test-fixtures/spec/suite-v0.1/evidence.schema.json`. Governance data includes -profile, event ID, artifact ID and revision, state, accountable owner, author, -reviewer, blockers, lockout set, prior event, actor, timestamp, and -idempotency key. +prior and current state, reviewed subject UID and revision, accountable owner, +revision author, independent reviewer, blockers and dispositions, lockout and +eligible-author sets, escalation/deadlock status, replay disposition, gate +result, actor, timestamp, idempotency key, replay key, expected revision, and +result revision. ## 4. State machine @@ -50,26 +61,26 @@ Escalation uses: `rejected|reassignment-required -> escalated -> dispositioned` -Only an approved artifact with applicable external checks may become completed. +Only an approved review subject with applicable external checks may become completed. Completion does not imply provider merge unless a binding says so. ## 5. Blocking verdict contract -A rejection MUST identify artifact location, violated requirement or invariant, +A rejection MUST identify review-subject location, violated requirement or invariant, reproducible evidence or concrete failure scenario, and the minimum clearing condition. Preference-only feedback MUST be advisory and MUST NOT trigger lockout. ## 6. Reviewer independence and lockout state machine -The blocking reviewer MUST differ from the current artifact author. After a -rejection, the current author is added to the artifact revision-cycle lockout +The blocking reviewer MUST differ from the current revision author. After a +rejection, the current author is added to the review-subject revision-cycle lockout set and MUST NOT author, co-author, pair on, or direct the next revision. The reviewer MAY clarify the blocker but MUST NOT become revision author for the same cycle. The coordinator MUST select an eligible revision author not in the lockout set. -Lockout is scoped to the artifact and persists until a later revision is +Lockout is scoped to the review subject and persists until a later revision is approved or an authority records an explicit override. A rejected revision adds its author to the set. If no eligible author remains, the coordinator MUST escalate rather than silently readmit a locked-out author. @@ -93,9 +104,9 @@ records ownership transfer. ## 8. Failure, idempotency, and concurrency Invalid transitions, self-review, locked-out revision authors, missing blocker -evidence, stale artifact revisions, and duplicate event IDs fail closed. +evidence, stale reviewed-subject revisions, and duplicate event IDs fail closed. Repeating an idempotency key returns the original result. Concurrent events use -the prior event ID and artifact revision as compare-and-swap conditions. +the prior event ID and reviewed-subject revision as compare-and-swap conditions. ## 9. Versioning and extensions @@ -110,8 +121,10 @@ reviewers MUST be authenticated by the executing environment. Records SHOULD reference evidence rather than duplicate secrets, private logs, or personal data. -## 11. Conformance +## 11. Promotion criteria -Conformance requires fixtures for approval, advisory feedback, rejection, -lockout, repeated rejection, reassignment, escalation, deadlock, stale events, -duplicate retries, and external-check gating. +Publish a transition manifest with positive and negative cases for approval, +advisory feedback, rejection, lockout, repeated rejection, reassignment, +eligible revision authors, escalation, deadlock, stale CAS, duplicate replay, +blocker disposition, and external-check gating. The registered rejection JSON +is an example fixture, not a conformance manifest. diff --git a/docs/specification/interoperability-conventions-v0.1.md b/docs/specification/interoperability-conventions-v0.1.md index b26d4368c..0fdd582f5 100644 --- a/docs/specification/interoperability-conventions-v0.1.md +++ b/docs/specification/interoperability-conventions-v0.1.md @@ -4,11 +4,20 @@ **Status:** Working Draft -**Document class:** Normative +**Document class:** Normative shared-schema draft **Capability identifier:** `squad-interop/v0.1` -**Maturity:** Stable normative dependency +**Maturity:** Experimental schema; non-claimable + +**Claimable capabilities:** None + +**Implementation status:** The shared evidence schema is executable. Discovery, +negotiation, retention, and cross-implementation conformance manifests are not +published. + +**Known deviations:** Current implementations do not expose one discovery +document, authority assertion shape, or replay-retention policy. **Depends on:** `squad-core/v0.1` @@ -25,7 +34,7 @@ An implementation MUST expose a discovery document or API result containing: - supported core, convention, profile, and binding identifiers; - exact capability identifiers and operations; -- supported artifact or message schema versions; +- supported file, record, or message schema versions; - canonical, compatible, and migration-only behavior classes; - extension namespaces owned or understood; - provider assumptions and implementation-policy boundaries. @@ -36,13 +45,14 @@ compatibility. ## 3. Stable identity and authority -Every actor, member, service, and artifact has an immutable UID scoped by an +Every actor, member, service, and portable resource has an immutable UID scoped by an issuer. Display names, member IDs used as paths, roles, casting personas, and provider logins are attributes and MUST NOT be treated as the authority UID. -An authority decision MUST identify actor UID, issuer, authenticated provider -identity, authority basis, action, resource UID, and policy revision. Casting -or persona changes MUST NOT alter authority. +Neutral observations identify the actor UID without implying authority. When +an authority assertion is present, it MUST identify action, issuer, +authenticated provider identity, authority basis, resource UID, and policy +revision. Casting or persona changes MUST NOT alter authority. ## 4. Shared event and evidence envelope @@ -73,7 +83,7 @@ leases, and retry limits MUST be declared by the executing capability. ## 6. Canonical privacy and secrets exclusion -Portable artifacts and evidence MUST NOT contain credentials, access tokens, +Portable files, records, and evidence MUST NOT contain credentials, access tokens, private keys, session cookies, secret environment values, hidden prompts, unredacted sensitive tool output, or provider authorization headers. @@ -99,18 +109,24 @@ versions assumed. Runtime drift outside the declared range returns `unsupported-provider-version` or `provider-drift`; it MUST NOT be silently treated as compatible. -## 9. Conformance classes +## 9. Capability identifiers, classes, and implementation roles -Capabilities claim one or more classes: producer, consumer, editor, validator, -executor, recorder, or provider binding. Claims are operation-specific and -MUST include canonical versus compatible behavior. +A claimable capability identifier has exactly this grammar: +`squad-/v./`, where each name component uses +lowercase ASCII letters, digits, and hyphens. The suite does not define a +second `#operation` alias. -The portable suite uses the externally claimable classes `artifact-only`, -`validator`, `editor`, `state-machine-executor`, `provider-binding`, and -`runtime`. The suite index at -`test-fixtures/spec/suite-v0.1/index.json` records exact profile-prefixed -capabilities, dependencies, dialect, maturity, manifests, and schemas. Partial -claims are valid only when all applicable cases and dependency claims pass. +The conformance-class vocabulary is `canonical`, `compatible`, `legacy`, and +`runtime`. Classes describe the behavior evidenced by cases; they are not +implementation roles. The implementation-role vocabulary is `producer`, +`consumer`, `editor`, `validator`, and `executor`. A role alone is never a +claim. + +Only a profile with a schema-valid executable manifest may advertise +capabilities or conformance classes in the suite index. Requirements drafts +retain implementation roles for design traceability but advertise no claims. +The Charter v0.1 registration therefore uses its unchanged `/parse`, +`/validate`, `/edit`, `/legacy-consume`, and `/runtime` identifiers. Portable case kinds are `positive`, `negative`, `compatibility`, `legacy`, `round-trip`, `mutation`, `transition`, `replay`, `concurrency`, `provider`, @@ -118,14 +134,31 @@ and `security`. Runtime profiles SHOULD promote from requirements-only status using transition traces with injected clocks and IDs. Provider bindings SHOULD reference neutral case IDs and digests rather than duplicate lifecycle rules. -Every normative profile requires falsifiable fixtures and an independent +Every claimable normative profile requires falsifiable fixtures and an independent reviewer before publication. Passing a schema alone does not prove executor or authority conformance. -## 10. Precedents - -These conventions align conceptually with W3C conformance classes, Kubernetes -UID/name and discovery separation, OCI media-type negotiation, RFC 9110 -idempotency semantics, CloudEvents 1.0.2, in-toto/SLSA provenance, and JSON -Schema vocabularies. Those precedents are informative unless cited by a profile -as a normative reference. +## 10. Versioned references and precedents + +The evidence envelope normatively uses +[JSON Schema 2020-12 Core §8](https://json-schema.org/draft/2020-12/json-schema-core#section-8) +for identifiers and references, +[RFC 3339 §5.6](https://www.rfc-editor.org/rfc/rfc3339#section-5.6) for +timestamps, and +[RFC 9110 §9.2.2](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.2) as the +HTTP precedent for idempotent retry analysis. + +CloudEvents 1.0.2 +[§3.1](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#required-attributes), +Kubernetes UID/name separation, OCI media-type negotiation, and in-toto/SLSA +provenance are informative design precedents; they are not imported wholesale +into this schema. + +## 11. Promotion criteria + +Publish a manifest with neutral-observation, complete and incomplete authority, +privacy/redaction, expected/result revision, duplicate event, repeated +idempotency key, conflicting payload digest, replay, unsupported version, and +extension cases. Discovery and negotiation require a separate executable +document or API-result schema before this dependency can advertise a +capability. diff --git a/docs/specification/planning-activation-v0.1.md b/docs/specification/planning-activation-v0.1.md index ec7ffab7c..3f01161ca 100644 --- a/docs/specification/planning-activation-v0.1.md +++ b/docs/specification/planning-activation-v0.1.md @@ -4,11 +4,19 @@ **Status:** Working Draft -**Document class:** Normative unless marked informative +**Document class:** Non-normative requirements draft **Profile identifier:** `squad-planning-activation/v0.1` -**Maturity:** Experimental normative +**Maturity:** Requirements draft; non-claimable + +**Claimable capabilities:** None + +**Implementation status:** gh-aw emits observable planning records, but their +provider envelope and fast-path behavior are not a portable manifest. + +**Known deviations:** Safe-output formatting, comment lookup, and activation +limits remain provider-specific. **Depends on:** `squad-core/v0.1`, `squad-interop/v0.1`, `squad-team-routing/v0.1`, @@ -21,15 +29,15 @@ implementation decomposition, validation, acceptance, activation, and revision evidence. It does not standardize prompt text, issue-comment wording, estimation culture, or one provider's safe-output mechanism. -## 2. Artifact envelope +## 2. Planning record envelope -Each artifact MUST declare profile, kind, schema version, intent ID, artifact -ID, revision, predecessor IDs, producer, timestamp, content digest, and +Each planning record must declare profile, kind, schema version, intent ID, +record ID, revision, predecessor IDs, producer, timestamp, content digest, and idempotency key. Portable kinds are `intent`, `research`, `triage`, `program`, `implementation`, `validation`, `scope-acceptance`, `implementation-acceptance`, `activation`, and `lifecycle-state`. -An intent MAY reference inline content, a file, or a URL. Consumers MUST record +An intent may reference inline content, a file, or a URL. Consumers must record the content digest actually evaluated. A changed digest creates a new intent revision and invalidates dependent acceptance until revalidated. @@ -39,19 +47,19 @@ revision and invalidates dependent acceptance until revalidated. implementation-planning -> validating -> scope-accepted -> implementation-accepted -> activated` -Revision is allowed before activation and MUST identify artifacts superseded. -A validation failure returns to implementation planning. Acceptance MUST name +Revision is allowed before activation and must identify records superseded. +A validation failure returns to implementation planning. Acceptance must name the exact accepted revision. Activation requires both acceptances and a passing validation over the same dependency closure. ## 4. Decomposition and acceptance -Implementation tasks MUST have stable task IDs, acceptance criteria, dependency +Implementation tasks must have stable task IDs, acceptance criteria, dependency IDs, accountable route, and activation disposition. Sizing, milestones, and meeting style are informative unless selected by an extension. -Human or delegated authority MUST be recorded for acceptance. Mere artifact -existence, model output, or a passing validator MUST NOT be treated as human +Human or delegated authority must be recorded for acceptance. Mere record +existence, model output, or a passing validator must not be treated as human approval. PRD prose quality, completeness, and preferred decomposition style are @@ -61,15 +69,15 @@ acceptance, and activation transitions are normative. ## 5. Activation Activation maps accepted task IDs to provider work-item references. Partial -activation MUST record the selected phase and remaining tasks. A task MUST NOT +activation must record the selected phase and remaining tasks. A task must not be created twice for the same activation idempotency key. Unresolved temporary references fail closed. -## 6. Capabilities and roles +## 6. Candidate operations and roles -Capabilities are `planning.produce`, `planning.validate`, +Candidate operations are `planning.produce`, `planning.validate`, `planning.accept-scope`, `planning.accept-implementation`, and -`planning.activate`. Producers create artifacts; validators check structure +`planning.activate`. Producers create records; validators check structure and dependency closure; acceptors exercise authority; activators create work. ## 7. Diagnostics and failure semantics @@ -77,24 +85,23 @@ and dependency closure; acceptors exercise authority; activators create work. Missing intent, stale digest, unknown dependency, cyclic dependency, unroutable task, failed validation, mismatched acceptance revision, unresolved reference, capacity limit, and partial provider mutation are distinct failures. Partial -mutation MUST return created references and a resumable boundary. +mutation must return created references and a resumable boundary. ## 8. Idempotency, retry, and concurrency Draft stages are replaceable revisions, not destructive edits. Acceptance records are immutable. Activation uses compare-and-swap on accepted revisions -and provider idempotency keys. Concurrent activation attempts MUST converge on +and provider idempotency keys. Concurrent activation attempts must converge on one mapping or fail with an explicit conflict. ## 9. Versioning, extensions, and security Artifact schema versions negotiate independently. Namespaced kinds and fields round-trip. Intake content is untrusted; links, commands, and embedded prompts -MUST NOT execute merely because they appear in a PRD. Secrets SHOULD be -referenced, not copied into artifacts. +must not execute merely because they appear in a PRD. Secrets should be referenced, not copied into planning records. -## 10. Conformance +## 10. Promotion criteria -Fixtures MUST cover granular and fast paths, PRD changes, validation failure, -acceptance revision mismatch, partial activation, duplicate retry, concurrent -activation, and dependency cycles. +Publish a manifest covering granular and fast paths, intent changes, validation +failure, acceptance revision mismatch, partial activation, duplicate retry, +concurrent activation, provider limits, and dependency cycles. diff --git a/docs/specification/state-memory-requirements-v0.1.md b/docs/specification/state-memory-requirements-v0.1.md index aab17e4a5..b770522b1 100644 --- a/docs/specification/state-memory-requirements-v0.1.md +++ b/docs/specification/state-memory-requirements-v0.1.md @@ -6,7 +6,15 @@ **Requirements identifier:** `squad-state-memory-requirements/v0.1` -**Maturity:** Informative and volatile +**Maturity:** Informative draft; non-claimable + +**Claimable capabilities:** None + +**Implementation status:** File, orphan-ref, two-layer, external, and memory +providers exist with different authority and recovery semantics. + +**Known deviations:** No backend-neutral CAS, recovery, migration, privacy, +retention, deletion, or cross-tenant fixture corpus exists. **Depends on:** `squad-core/v0.1`, `squad-interop/v0.1` @@ -18,7 +26,7 @@ provider-specific search. These implementations expose useful seams, but they do not yet share enough observable recovery and authority evidence for a truthful normative schema. -## 2. Required future semantics +## 2. State authority requirements A promoted profile needs to define: @@ -29,31 +37,39 @@ A promoted profile needs to define: - append, replace, delete, archive, and tombstone semantics; - startup verification, degraded mode, circuit breaking, and recovery; - migration and rollback between backends without split-brain authority; -- memory classification before provider dispatch; -- provider availability, search provenance, deletion, retention, and audit; - trust boundaries among repository state, host-injected services, and remote providers; - encryption, secrets, privacy, redaction, and data residency declarations. -## 3. Provisional authority rules +## 3. Memory classification requirements + +A promoted memory profile needs stable classes before provider dispatch, +provider availability and search provenance, retention and deletion behavior, +audit evidence, and an explicit rule preventing forbidden or transient content +from reaching durable providers. + +State-authority promotion does not imply memory-classification promotion, and +memory promotion does not imply backend migration conformance. + +## 4. Provisional authority rules Static configuration and normative artifacts remain file-authoritative unless a -profile says otherwise. Mutable state MUST have exactly one authority backend -per collection. Mirrors and caches MUST identify their source revision and MUST +profile says otherwise. Mutable state must have exactly one authority backend +per collection. Mirrors and caches must identify their source revision and must NOT accept writes while authority is ambiguous. -Memory classification MUST precede durable or external writes. Forbidden and -transient content MUST NOT reach a durable provider. Provider failures MUST be -observable and MUST NOT silently convert a denied write into local success. +Memory classification must precede durable or external writes. Forbidden and +transient content must not reach a durable provider. Provider failures must be +observable and must not silently convert a denied write into local success. -## 4. Candidate portable operations +## 5. Candidate portable operations The likely operation set is `read`, `list`, `compare-and-swap-write`, `append`, `delete`, `archive`, `health`, `migrate`, `recover`, `classify`, `search`, and `promote`. Each operation needs request IDs, actor, expected revision, result revision, backend identity, diagnostics, and idempotency semantics. -## 5. Security and privacy requirements +## 6. Security and privacy requirements Backends must prevent path traversal and unauthorized ref access. Audit records should omit content by default. External providers require explicit enablement, @@ -61,7 +77,7 @@ authorization, retention disclosure, deletion behavior, and secret filtering. Recovery tools must not leak state from another repository, worktree, user, or account. -## 6. Promotion criteria +## 7. Promotion criteria This document may become `squad-state-memory/v0.1` only after: @@ -70,11 +86,13 @@ This document may become `squad-state-memory/v0.1` only after: 2. compare-and-swap, duplicate retry, crash recovery, migration, rollback, and split-brain tests have common expected results; 3. authority discovery and degraded-mode behavior are independently specified; -4. memory classification and provider dispatch have stable vocabularies; -5. a security review covers traversal, ref isolation, secrets, retention, +4. state authority has a standalone manifest independent of memory providers; +5. memory classification and provider dispatch have a separate stable + vocabulary and fixture set; +6. a security review covers traversal, ref isolation, secrets, retention, deletion, and cross-tenant leakage. -## 7. Intentionally deferred details +## 8. Intentionally deferred details Git plumbing, SQLite layout, index algorithms, embeddings, ranking, cache format, circuit-breaker constants, prompt-only fallback, and provider product diff --git a/docs/specification/team-routing-v0.1.md b/docs/specification/team-routing-v0.1.md index 381d99bb0..1674e4060 100644 --- a/docs/specification/team-routing-v0.1.md +++ b/docs/specification/team-routing-v0.1.md @@ -2,11 +2,21 @@ **Status:** Working Draft -**Document class:** Normative unless marked informative +**Document class:** Non-normative requirements draft **Profile identifier:** `squad-team-routing/v0.1` -**Maturity:** Stable normative +**Maturity:** Requirements draft; non-claimable + +**Claimable capabilities:** None + +**Implementation status:** Roster and routing files are implemented, but no +portable manifest covers their distinct parsing, mutation, join, and resolution +contracts. + +**Known deviations:** Existing routing files do not share one fallback, +priority, or ambiguity representation. See +[Artifact Inventory: Known unstable behavior](artifact-inventory.md#known-unstable-and-non-conforming-behavior). **Depends on:** `squad-core/v0.1`, `squad-interop/v0.1`, `squad-charter/v0.1` @@ -15,8 +25,6 @@ **Feedback:** Issue #2069 -BCP 14 requirement keywords apply only when written in all capitals. - ## 1. Scope and non-goals This profile defines roster identity, accountable roles, capability ownership, @@ -35,69 +43,75 @@ coordinator executes route decisions. Authority uses the immutable actor UID from Interoperability Conventions, not a member ID, display name, role, or casting persona. -## 3. Canonical artifacts +## 3. Roster requirements -`team.md` MUST contain a `Members` table with `ID`, `Role`, `Charter`, and -`Status` semantics. Compatible headings and columns MAY vary, but validators -MUST expose their mapping. Every active member MUST reference exactly one +`team.md` must contain a `Members` table with `ID`, `Role`, `Charter`, and +`Status` semantics. Compatible headings and columns may vary, but validators +must expose their mapping. Every active member must reference exactly one existing charter whose identity matches the member ID. -`routing.md` MUST contain a `Routing Table` with `Work Type`, `Route To`, and -optional `Examples`. `Route To` MUST name exactly one active member. Support -identities and coding agents MAY appear in the roster but MUST NOT be routing +Roster promotion is independent of route-resolution promotion. + +## 4. Routing requirements + +`routing.md` must contain a `Routing Table` with `Work Type`, `Route To`, and +optional `Examples`. `Route To` must name exactly one active member. Support +identities and coding agents may appear in the roster but must not be routing destinations unless the team manifest explicitly grants accountable ownership. -The suite index declares artifact-only conformance until a profile-specific -manifest supplies document, mutation, and route-resolution cases. +The suite index advertises no capability for this draft. -## 4. Deterministic routing +## 5. Deterministic routing 1. An explicit, authorized member assignment wins. 2. Otherwise the highest-priority matching route wins. -3. Equal-priority matches are an ambiguity failure; consumers MUST NOT select +3. Equal-priority matches are an ambiguity failure; consumers must not select by file order unless the profile extension declares that policy. 4. No match uses the declared fallback member. 5. No fallback produces `route-unresolved` and requires coordinator or human disposition. -Classifiers MAY use different algorithms, but route evidence MUST identify the +Classifiers may use different algorithms, but route evidence must identify the selected rule, input revision, confidence, and fallback use. Generated regexes, embeddings, prompts, and ranking algorithms are non-normative. -## 5. Capabilities and behavior classes +## 6. Candidate operations and implementation roles -Capabilities are `team.produce`, `team.consume`, `team.edit`, `team.validate`, -`routing.resolve`, and `routing.explain`. Canonical producers emit one active -member row per ID and one primary owner per work type. Compatible consumers MAY +Candidate operations are `team.produce`, `team.consume`, `team.edit`, +`team.validate`, `routing.resolve`, and `routing.explain`. These operation names +are design vocabulary, not claimable capability identifiers. Canonical producers emit one active +member row per ID and one primary owner per work type. Compatible consumers may accept legacy display-name destinations if they resolve unambiguously. -## 6. Diagnostics and failures +## 7. Diagnostics and failures -Validators MUST report stable diagnostics for duplicate IDs, missing charters, +Validators must report stable diagnostics for duplicate IDs, missing charters, identity mismatch, unknown destinations, inactive destinations, duplicate work -types, ambiguous priorities, and absent fallback. Invalid topology MUST fail +types, ambiguous priorities, and absent fallback. Invalid topology must fail closed for execution while remaining available for diagnostic display. -## 7. Idempotency, retry, and concurrency +## 8. Idempotency, retry, and concurrency -Applying the same roster or route revision is idempotent. Editors MUST use a -revision or content digest for concurrent updates and MUST reject stale writes. -Resolution is read-only and MAY retry after reloading a newer revision. +Applying the same roster or route revision is idempotent. Editors must use a +revision or content digest for concurrent updates and must reject stale writes. +Resolution is read-only and may retry after reloading a newer revision. -## 8. Versioning, extensions, and discovery +## 9. Versioning, extensions, and discovery Profile selection is explicit. `X-` Markdown sections and namespaced topology -properties are extensions and MUST round-trip unchanged. Consumers MUST fail +properties are extensions and must round-trip unchanged. Consumers must fail closed on unsupported profile versions and unknown mandatory capabilities. -## 9. Security, privacy, and authority +## 10. Security, privacy, and authority Roster membership and routing do not grant provider authorization, repository write access, tools, secrets, or model entitlement. Artifact text is untrusted. -Producers SHOULD avoid personal data in roles and examples. +Producers should avoid personal data in roles and examples. -## 10. Conformance +## 11. Promotion criteria -Conformance requires schema-valid topology fixtures, charter joins, ambiguity -fixtures, stale-revision tests, and deterministic route evidence. A resolver -MUST publish the exact capabilities it implements. +Roster promotion requires schema-valid document and mutation cases, charter +joins, extension round trips, and stable diagnostics. Routing promotion +separately requires ambiguity, priority, fallback, stale-revision, and +deterministic route-evidence cases. Only a future manifest may introduce +claimable capability identifiers. diff --git a/docs/specification/work-lifecycle-v0.1.md b/docs/specification/work-lifecycle-v0.1.md index 1122d9ae1..71386829d 100644 --- a/docs/specification/work-lifecycle-v0.1.md +++ b/docs/specification/work-lifecycle-v0.1.md @@ -4,11 +4,19 @@ **Status:** Working Draft -**Document class:** Normative unless marked informative +**Document class:** Non-normative requirements draft **Profile identifier:** `squad-work-lifecycle/v0.1` -**Maturity:** Experimental normative +**Maturity:** Requirements draft; non-claimable + +**Claimable capabilities:** None + +**Implementation status:** The state vocabulary is a provider-neutral design; +no portable transition manifest or cross-provider trace corpus exists. + +**Known deviations:** Current providers do not emit one shared revision, +recovery, cancellation, or blocker-resume record. **Depends on:** `squad-core/v0.1`, `squad-interop/v0.1`, `squad-team-routing/v0.1`, @@ -34,19 +42,45 @@ The closed finite work states are `untriaged`, `assigned`, `in-progress`, `needs All transitions not explicitly listed by this profile are invalid. -## 3. Deterministic transitions - -- `untriaged -> assigned` requires one accountable route. -- `assigned -> in-progress` requires a claimed workspace. -- `in-progress -> needs-review` requires a change reference and evidence. -- `needs-review -> changes-requested` follows a blocking governance verdict. -- `needs-review|changes-requested|checks-failed -> ready` requires approval of - the current revision and all required checks passing. -- `ready -> integrated` requires provider integration evidence. -- `integrated -> done` requires closure disposition and dependency updates. - -Provider observations MAY arrive out of order. Consumers MUST reconcile by -resource revision and MUST NOT infer integration from closure alone. +## 3. Complete transition table + +| From | To | Guard and evidence | Recovery or terminal behavior | +|---|---|---|---| +| `untriaged` | `assigned` | Exactly one accountable route | Re-route by a new assignment revision | +| `untriaged` | `cancelled` | Authorized cancellation disposition | Terminal | +| `assigned` | `in-progress` | Claimed workspace at expected work revision | Release or replace a stale claim | +| `assigned` | `blocked` | Active dependency or authority blocker | Resume to `assigned` after blocker clearance | +| `assigned` | `cancelled` | Authorized cancellation disposition | Terminal | +| `in-progress` | `needs-review` | Current change reference and submitted revision | Return to `in-progress` for a newer change revision | +| `in-progress` | `blocked` | Active dependency, workspace, or authority blocker | Resume to `in-progress` | +| `in-progress` | `cancelled` | Authorized cancellation disposition | Terminal | +| `needs-review` | `changes-requested` | Blocking verdict against the current revision | Resume through `in-progress` with a new revision | +| `needs-review` | `checks-failed` | One or more required checks failed | Re-run checks or submit a new revision | +| `needs-review` | `ready` | Independent approval and all required checks pass for one revision | Regress if review or check evidence changes | +| `needs-review` | `blocked` | External dependency or authority blocker | Resume to `needs-review` | +| `needs-review` | `cancelled` | Authorized cancellation disposition | Terminal | +| `changes-requested` | `in-progress` | Eligible revision author claims the correction | Submit a new revision for review | +| `changes-requested` | `blocked` | Revision cannot proceed | Resume to `changes-requested` | +| `changes-requested` | `cancelled` | Authorized cancellation disposition | Terminal | +| `checks-failed` | `in-progress` | Failure requires a code or configuration revision | Submit the corrected revision | +| `checks-failed` | `needs-review` | Checks are re-running for the unchanged revision | Await composed review/check result | +| `checks-failed` | `ready` | The same revision now has approval and passing required checks | Regress on newer failure or review | +| `checks-failed` | `blocked` | Check infrastructure or dependency blocker | Resume to `checks-failed` | +| `checks-failed` | `cancelled` | Authorized cancellation disposition | Terminal | +| `blocked` | prior nonterminal state | All recorded blockers clear and `resumeState` names the prior state | A missing or stale `resumeState` is invalid | +| `ready` | `changes-requested` | A current-revision blocking verdict supersedes approval | Correct through `in-progress` | +| `ready` | `checks-failed` | A required check fails or is invalidated | Re-run or revise | +| `ready` | `blocked` | Integration dependency or authority blocker | Resume to `ready` only if evidence remains current | +| `ready` | `integrated` | Provider integration succeeds at expected revision | Continue to closure | +| `ready` | `cancelled` | Authorized cancellation before integration | Terminal | +| `integrated` | `done` | Closure disposition and dependency updates succeed | Terminal | + +`done` and `cancelled` are terminal. Every transition not listed above is +invalid. `blocked` records both blockers and the exact `resumeState`; it is not +a wildcard transition. + +Provider observations may arrive out of order. Consumers must reconcile by +resource revision and must not infer integration from closure alone. ## 4. Dependencies and workspaces @@ -55,9 +89,9 @@ an extension explicitly permits informational relationships. A workspace claim records base revision, branch or workspace reference, actor, and lease. Worktrees are one compatible workspace implementation, not a requirement. -## 5. Capabilities and roles +## 5. Candidate operations and roles -Capabilities are `work.observe`, `work.assign`, `work.claim`, +Candidate operations are `work.observe`, `work.assign`, `work.claim`, `work.change-submit`, `work.check`, `work.review`, `work.integrate`, and `work.close`. Provider bindings declare which operations they implement. @@ -65,25 +99,27 @@ Capabilities are `work.observe`, `work.assign`, `work.claim`, Unroutable assignment, dependency cycle, stale workspace base, duplicate active claim, missing change, stale review, failed required check, unauthorized merge, -partial integration, and closure mismatch are distinct failures. Failures MUST +partial integration, and closure mismatch are distinct failures. Failures must retain provider references and recovery guidance. ## 7. Idempotency, retry, and concurrency Every mutation carries an operation ID. Repeated operations return the same resource mapping. Workspace and integration operations require expected -revisions. Partial provider mutations are resumable and MUST NOT be reported as +revisions. Partial provider mutations are resumable and must not be reported as success-shaped completion. ## 8. Versioning, extensions, security, and privacy Bindings negotiate separately from this profile. Namespaced states and fields round-trip. Work descriptions, review comments, branches, and check logs are -untrusted. Provider credentials and secret logs MUST NOT enter lifecycle +untrusted. Provider credentials and secret logs must not enter lifecycle records. -## 9. Conformance +## 9. Promotion criteria -Conformance requires traces for happy path, blocked dependencies, stale base, -review rejection and lockout, check failure and recovery, duplicate retries, -partial integration, cancellation, and provider event reordering. +Publish a manifest with positive and negative traces for every table row, +blocked-state recovery, stale base, rejection and lockout, check failure, +duplicate retry, partial integration, cancellation, terminal-state rejection, +and provider event reordering. Run the same neutral traces against GitHub and +one non-GitHub binding. diff --git a/package.json b/package.json index 12523d04e..c3a5d2fbf 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "lint": "tsc --noEmit -p packages/squad-sdk/tsconfig.json && tsc --noEmit -p packages/squad-cli/tsconfig.json", "fix:crlf": "node scripts/fix-crlf-worktree.mjs", "lint:eslint": "eslint packages/ test/", - "lint:docs": "markdownlint-cli2 && cspell --no-progress --dot \"docs/src/content/**/*.md\" \"README.md\"", + "lint:docs": "markdownlint-cli2 \"docs/src/content/**/*.md\" \"docs/specification/**/*.md\" \"README.md\" && cspell --no-progress --dot \"docs/src/content/**/*.md\" \"docs/specification/**/*.md\" \"README.md\"", "dev:link": "npm run build && cd packages/squad-sdk && npm link && cd ../squad-cli && npm link", "dev:unlink": "cd packages/squad-cli && npm unlink && cd ../squad-sdk && npm unlink", "docs:api": "node scripts/generate-api-docs.mjs", diff --git a/test-fixtures/spec/suite-v0.1/evidence.schema.json b/test-fixtures/spec/suite-v0.1/evidence.schema.json index e7192fb7b..8d7495703 100644 --- a/test-fixtures/spec/suite-v0.1/evidence.schema.json +++ b/test-fixtures/spec/suite-v0.1/evidence.schema.json @@ -38,12 +38,33 @@ "actor": { "type": "object", "additionalProperties": false, - "required": ["uid", "issuer", "providerIdentity", "authorityBasis"], + "required": ["uid"], "properties": { "uid": { "type": "string", "minLength": 1 }, + "displayName": { "type": "string", "minLength": 1 } + } + }, + "authority": { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "issuer", + "providerIdentity", + "basis", + "policyRevision" + ], + "properties": { + "action": { "type": "string", "minLength": 1 }, "issuer": { "type": "string", "minLength": 1 }, "providerIdentity": { "type": "string", "minLength": 1 }, - "authorityBasis": { "type": "string", "minLength": 1 } + "basis": { "type": "string", "minLength": 1 }, + "policyRevision": { + "anyOf": [ + { "type": "string", "minLength": 1 }, + { "type": "integer" } + ] + } } }, "resource": { @@ -93,5 +114,224 @@ "propertyNames": { "pattern": "^[a-z0-9]+:[a-z0-9][a-z0-9.-]*$" } }, "data": { "type": "object" } + }, + "allOf": [ + { + "if": { + "properties": { + "schema": { "const": "squad-governance-review/v0.1" } + }, + "required": ["schema"] + }, + "then": { + "required": ["authority", "replayKey"], + "properties": { + "authority": { "$ref": "#/properties/authority" }, + "replayKey": { "$ref": "#/properties/replayKey" }, + "resource": { + "type": "object", + "required": [ + "uid", + "revision", + "expectedRevision", + "resultRevision" + ], + "properties": { + "uid": { "$ref": "#/properties/resource/properties/uid" }, + "revision": { "$ref": "#/properties/resource/properties/revision" }, + "expectedRevision": { "$ref": "#/properties/resource/properties/expectedRevision" }, + "resultRevision": { "$ref": "#/properties/resource/properties/resultRevision" } + } + }, + "data": { "$ref": "#/$defs/governanceTransition" } + } + } + } + ], + "$defs": { + "uid": { "type": "string", "minLength": 1 }, + "revision": { + "anyOf": [ + { "type": "string", "minLength": 1 }, + { "type": "integer" } + ] + }, + "blocker": { + "type": "object", + "additionalProperties": false, + "required": [ + "code", + "location", + "requirement", + "evidence", + "clearCondition", + "status" + ], + "properties": { + "code": { "type": "string", "minLength": 1 }, + "location": { "type": "string", "minLength": 1 }, + "requirement": { "type": "string", "minLength": 1 }, + "evidence": { "type": "string", "minLength": 1 }, + "clearCondition": { "type": "string", "minLength": 1 }, + "status": { "enum": ["open", "cleared", "waived"] } + } + }, + "governanceTransition": { + "type": "object", + "additionalProperties": false, + "required": [ + "transition", + "reviewedSubject", + "accountableOwnerUid", + "revisionAuthorUid", + "reviewerUid", + "blockers", + "lockoutUids", + "eligibleRevisionAuthorUids", + "escalation", + "replayDisposition", + "gateResult" + ], + "properties": { + "transition": { + "type": "object", + "additionalProperties": false, + "required": ["priorState", "currentState"], + "properties": { + "priorState": { + "enum": [ + "draft", + "submitted", + "under-review", + "rejected", + "reassignment-required", + "revising", + "approved", + "completed", + "escalated", + "dispositioned" + ] + }, + "currentState": { + "enum": [ + "draft", + "submitted", + "under-review", + "rejected", + "reassignment-required", + "revising", + "approved", + "completed", + "escalated", + "dispositioned" + ] + } + }, + "oneOf": [ + { + "properties": { + "priorState": { "const": "draft" }, + "currentState": { "const": "submitted" } + } + }, + { + "properties": { + "priorState": { "const": "submitted" }, + "currentState": { "const": "under-review" } + } + }, + { + "properties": { + "priorState": { "const": "under-review" }, + "currentState": { "enum": ["approved", "rejected"] } + } + }, + { + "properties": { + "priorState": { "const": "rejected" }, + "currentState": { + "enum": ["reassignment-required", "escalated"] + } + } + }, + { + "properties": { + "priorState": { "const": "reassignment-required" }, + "currentState": { "enum": ["revising", "escalated"] } + } + }, + { + "properties": { + "priorState": { "const": "revising" }, + "currentState": { "const": "submitted" } + } + }, + { + "properties": { + "priorState": { "const": "approved" }, + "currentState": { "const": "completed" } + } + }, + { + "properties": { + "priorState": { "const": "escalated" }, + "currentState": { "const": "dispositioned" } + } + } + ] + }, + "reviewedSubject": { + "type": "object", + "additionalProperties": false, + "required": ["uid", "revision"], + "properties": { + "uid": { "$ref": "#/$defs/uid" }, + "revision": { "$ref": "#/$defs/revision" } + } + }, + "accountableOwnerUid": { "$ref": "#/$defs/uid" }, + "revisionAuthorUid": { "$ref": "#/$defs/uid" }, + "reviewerUid": { "$ref": "#/$defs/uid" }, + "blockers": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/blocker" } + }, + "lockoutUids": { + "type": "array", + "items": { "$ref": "#/$defs/uid" }, + "uniqueItems": true + }, + "eligibleRevisionAuthorUids": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/uid" }, + "uniqueItems": true + }, + "escalation": { + "type": "object", + "additionalProperties": false, + "required": ["status", "deadlocked"], + "properties": { + "status": { "enum": ["not-required", "required", "resolved"] }, + "deadlocked": { "type": "boolean" }, + "authorityUid": { + "anyOf": [ + { "$ref": "#/$defs/uid" }, + { "type": "null" } + ] + }, + "reason": { + "anyOf": [ + { "type": "string", "minLength": 1 }, + { "type": "null" } + ] + } + } + }, + "replayDisposition": { "enum": ["original", "duplicate-same-result"] }, + "gateResult": { "enum": ["accepted", "rejected", "conflict", "escalated"] } + } + } } } diff --git a/test-fixtures/spec/suite-v0.1/governance-rejection.example.json b/test-fixtures/spec/suite-v0.1/governance-rejection.example.json index 748025594..25e59e1db 100644 --- a/test-fixtures/spec/suite-v0.1/governance-rejection.example.json +++ b/test-fixtures/spec/suite-v0.1/governance-rejection.example.json @@ -4,42 +4,62 @@ "id": "1416ae81-91ef-48b3-b6fa-9b810b86b012", "type": "squad.governance.rejected", "source": "github:bradygaster/squad", - "subject": "artifact:change-request/2074", + "subject": "review-subject:change-request/2078", "time": "2026-09-25T14:17:33Z", "actor": { - "uid": "squad-member:unit-contract-tester", + "uid": "squad-member:docs-devrel" + }, + "authority": { + "action": "review.reject", "issuer": "squad:bradygaster/squad", "providerIdentity": "github:reviewer-login", - "authorityBasis": "review-gate:contract" + "basis": "review-gate:publication", + "policyRevision": "governance-2026-09-25" }, "resource": { - "uid": "artifact:change-request/2074", - "revision": "71634fde", - "expectedRevision": "71634fde", - "resultRevision": "71634fde" + "uid": "review-subject:change-request/2078", + "revision": "c03ba950fde071c8ec58a64504fdb7b5cf1206ef", + "expectedRevision": "c03ba950fde071c8ec58a64504fdb7b5cf1206ef", + "resultRevision": "c03ba950fde071c8ec58a64504fdb7b5cf1206ef" }, "correlationId": "8d1f7875-b855-4996-93fd-fee67c775188", "causationId": null, - "idempotencyKey": "review:2074:71634fde:unit-contract-tester", - "replayKey": "github-review:2074:71634fde:1", + "idempotencyKey": "review:2078:c03ba950:docs-devrel", + "replayKey": "github-review:2078:c03ba950:1", "payloadDigest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "redaction": "metadata-only", "extensions": {}, "data": { - "state": "rejected", - "lockoutState": "author-locked", + "transition": { + "priorState": "under-review", + "currentState": "rejected" + }, + "reviewedSubject": { + "uid": "github:bradygaster/squad#2078", + "revision": "c03ba950fde071c8ec58a64504fdb7b5cf1206ef" + }, "accountableOwnerUid": "squad-member:architect", - "authorUid": "squad-member:architect", - "reviewerUid": "squad-member:unit-contract-tester", + "revisionAuthorUid": "squad-member:architect", + "reviewerUid": "squad-member:docs-devrel", "lockoutUids": ["squad-member:architect"], + "eligibleRevisionAuthorUids": ["squad-member:agent-orchestration-dev"], "blockers": [ { - "code": "missing-conformance-fixture", - "location": "test-fixtures/spec/suite-v0.1", - "requirement": "Every normative profile has validator or fixture evidence", - "evidence": "No shared envelope fixture was present", - "clearCondition": "Add and validate one language-neutral evidence fixture" + "code": "publication-maturity-overclaim", + "location": "docs/specification/README.md", + "requirement": "Requirements-only drafts are non-claimable", + "evidence": "The rejected index advertised capabilities without manifests", + "clearCondition": "Downgrade unsupported claims and publish promotion criteria", + "status": "open" } - ] + ], + "escalation": { + "status": "not-required", + "deadlocked": false, + "authorityUid": null, + "reason": null + }, + "replayDisposition": "original", + "gateResult": "rejected" } } diff --git a/test-fixtures/spec/suite-v0.1/index.json b/test-fixtures/spec/suite-v0.1/index.json index 6e77a9369..11deed3b8 100644 --- a/test-fixtures/spec/suite-v0.1/index.json +++ b/test-fixtures/spec/suite-v0.1/index.json @@ -9,16 +9,16 @@ }, "dialects": [ { - "id": "charter-v0-1-legacy", - "description": "Existing Charter v0.1 manifest dialect, registered unchanged." + "id": "charter-v0-1", + "description": "Published Charter v0.1 executable conformance manifest." }, { "id": "suite-v0-1", - "description": "Reusable suite index and shared event/evidence schemas." + "description": "Experimental shared schemas and explicitly registered fixtures." }, { "id": "requirements-only", - "description": "No portable executable manifest yet; promotion criteria are normative planning input only." + "description": "Non-claimable conceptual coverage with explicit promotion criteria." } ], "sharedSchemas": [ @@ -27,162 +27,263 @@ "profiles": [ { "id": "squad-core/v0.1", - "maturity": "stable-normative", + "maturity": "requirements-draft", + "documentClass": "requirements", "spec": "docs/specification/core-v0.1.md", "dialect": "requirements-only", "dependencies": [], - "classes": ["artifact-only"], - "capabilities": ["squad-core/v0.1#consume"], + "conformanceClasses": [], + "implementationRoles": ["producer", "consumer", "editor", "validator", "executor"], + "capabilities": [], "manifest": null, - "manifestSchema": null + "manifestSchema": null, + "fixtures": [], + "implementationStatus": "Shared terminology is used by Charter v0.1; no standalone Core manifest exists.", + "knownDeviations": ["Publication and canonical JSON rules remain incomplete."], + "promotionCriteria": ["Publish a standalone manifest covering paths, versions, diagnostics, and trust rules."], + "entryPoint": "Core terminology, then Charter v0.1 for executable behavior." }, { "id": "squad-interop/v0.1", - "maturity": "stable-normative", + "maturity": "experimental-schema", + "documentClass": "normative-schema", "spec": "docs/specification/interoperability-conventions-v0.1.md", "dialect": "suite-v0-1", "dependencies": ["squad-core/v0.1"], - "classes": ["artifact-only", "validator"], - "capabilities": ["squad-interop/v0.1#evidence.validate"], + "conformanceClasses": [], + "implementationRoles": ["producer", "consumer", "validator", "executor"], + "capabilities": [], "manifest": null, - "manifestSchema": null + "manifestSchema": null, + "fixtures": [], + "implementationStatus": "The shared evidence schema is executable; discovery and negotiation are requirements only.", + "knownDeviations": ["No discovery manifest or cross-implementation evidence corpus exists."], + "promotionCriteria": ["Add a manifest with neutral observation, authority, replay, CAS, and privacy cases."], + "entryPoint": "Read after Core when implementing shared evidence." }, { "id": "squad-charter/v0.1", - "maturity": "stable-normative", + "maturity": "published-normative", + "documentClass": "normative-profile", "spec": "docs/specification/charter-v0.1.md", - "dialect": "charter-v0-1-legacy", + "dialect": "charter-v0-1", "dependencies": ["squad-core/v0.1"], - "classes": ["artifact-only", "validator", "editor", "runtime"], + "conformanceClasses": ["canonical", "compatible", "legacy", "runtime"], + "implementationRoles": ["producer", "consumer", "editor", "validator", "executor"], "capabilities": [ - "squad-charter/v0.1#validate", - "squad-charter/v0.1#parse", - "squad-charter/v0.1#serialize", - "squad-charter/v0.1#runtime-compile" + "squad-charter/v0.1/parse", + "squad-charter/v0.1/validate", + "squad-charter/v0.1/edit", + "squad-charter/v0.1/legacy-consume", + "squad-charter/v0.1/runtime" ], "manifest": "test-fixtures/spec/charter-v0.1/manifest.json", - "manifestSchema": "test-fixtures/spec/charter-v0.1/manifest.schema.json" + "manifestSchema": "test-fixtures/spec/charter-v0.1/manifest.schema.json", + "fixtures": [], + "implementationStatus": "Reference parser, validator, editor, runtime compiler, and portable cases are implemented.", + "knownDeviations": [], + "promotionCriteria": [], + "entryPoint": "Start here for executable artifact conformance." }, { "id": "squad-team-routing/v0.1", - "maturity": "stable-normative", + "maturity": "requirements-draft", + "documentClass": "requirements", "spec": "docs/specification/team-routing-v0.1.md", "dialect": "requirements-only", "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-charter/v0.1"], - "classes": ["artifact-only"], - "capabilities": ["squad-team-routing/v0.1#artifact"], + "conformanceClasses": [], + "implementationRoles": ["producer", "consumer", "editor", "validator", "executor"], + "capabilities": [], "manifest": null, - "manifestSchema": null + "manifestSchema": null, + "fixtures": [], + "implementationStatus": "Roster and routing are implemented in multiple shapes but have no portable manifest.", + "knownDeviations": ["Routing fallback and ambiguity policies are not represented uniformly."], + "promotionCriteria": ["Add separate roster and routing cases, joins, diagnostics, stale-write, and resolution evidence."], + "entryPoint": "Read roster requirements before routing requirements." }, { "id": "squad-configuration-casting/v0.1", - "maturity": "experimental-normative", + "maturity": "requirements-draft", + "documentClass": "requirements", "spec": "docs/specification/configuration-casting-v0.1.md", "dialect": "requirements-only", "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-team-routing/v0.1"], - "classes": ["artifact-only"], - "capabilities": ["squad-configuration-casting/v0.1#artifact"], + "conformanceClasses": [], + "implementationRoles": ["producer", "consumer", "editor", "validator", "executor"], + "capabilities": [], "manifest": null, - "manifestSchema": null + "manifestSchema": null, + "fixtures": [], + "implementationStatus": "Configuration, casting registry, and provenance exist but are not one portable profile.", + "knownDeviations": ["Legacy registry shapes and runtime-only configuration keys remain accepted."], + "promotionCriteria": ["Add independent configuration, casting, and provenance manifests with migration and CAS cases."], + "entryPoint": "Read configuration, casting identity, then provenance sections independently." }, { "id": "squad-governance-review/v0.1", - "maturity": "experimental-normative", + "maturity": "experimental-schema", + "documentClass": "normative-schema", "spec": "docs/specification/governance-review-v0.1.md", "dialect": "suite-v0-1", "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-team-routing/v0.1"], - "classes": ["state-machine-executor"], - "capabilities": ["squad-governance-review/v0.1#transition"], - "manifest": "test-fixtures/spec/suite-v0.1/governance-rejection.example.json", - "manifestSchema": "test-fixtures/spec/suite-v0.1/evidence.schema.json" + "conformanceClasses": [], + "implementationRoles": ["producer", "consumer", "validator", "executor"], + "capabilities": [], + "manifest": null, + "manifestSchema": null, + "fixtures": [ + "test-fixtures/spec/suite-v0.1/governance-rejection.example.json" + ], + "implementationStatus": "The evidence schema and representative rejection fixture are executable; no transition manifest exists.", + "knownDeviations": ["Repository governance previously permitted immediate owner self-revision."], + "promotionCriteria": ["Add a transition manifest covering approval, lockout, replay, CAS, blockers, escalation, and deadlock."], + "entryPoint": "Use the evidence contract for records; treat transition prose as draft requirements." }, { "id": "squad-execution-preferences/v0.1", - "maturity": "experimental-normative", + "maturity": "requirements-draft", + "documentClass": "requirements", "spec": "docs/specification/execution-preferences-v0.1.md", "dialect": "requirements-only", "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-charter/v0.1", "squad-configuration-casting/v0.1"], - "classes": ["artifact-only"], - "capabilities": ["squad-execution-preferences/v0.1#artifact"], + "conformanceClasses": [], + "implementationRoles": ["consumer", "validator", "executor"], + "capabilities": [], "manifest": null, - "manifestSchema": null + "manifestSchema": null, + "fixtures": [], + "implementationStatus": "Selection and fallback exist in the reference runtime only.", + "knownDeviations": ["Provider model names and fallback chains are implementation policy."], + "promotionCriteria": ["Add preference-resolution cases with policy, authorization, availability, and loss reporting."], + "entryPoint": "Use for portable intent design, not a current conformance claim." }, { "id": "squad-work-lifecycle/v0.1", - "maturity": "experimental-normative", + "maturity": "requirements-draft", + "documentClass": "requirements", "spec": "docs/specification/work-lifecycle-v0.1.md", "dialect": "requirements-only", "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-team-routing/v0.1", "squad-governance-review/v0.1"], - "classes": ["state-machine-executor"], - "capabilities": ["squad-work-lifecycle/v0.1#transition"], + "conformanceClasses": [], + "implementationRoles": ["producer", "consumer", "validator", "executor"], + "capabilities": [], "manifest": null, - "manifestSchema": null + "manifestSchema": null, + "fixtures": [], + "implementationStatus": "The state vocabulary is a provider-neutral design; no executable transition corpus exists.", + "knownDeviations": ["Current providers do not expose one shared revision or recovery model."], + "promotionCriteria": ["Add complete positive and negative transition traces against GitHub and one non-GitHub provider."], + "entryPoint": "Read before any provider binding." }, { "id": "squad-work-github/v0.1", - "maturity": "experimental-normative", + "maturity": "requirements-draft", + "documentClass": "requirements", "spec": "docs/specification/github-work-lifecycle-binding-v0.1.md", "dialect": "requirements-only", "dependencies": ["squad-interop/v0.1", "squad-work-lifecycle/v0.1", "squad-governance-review/v0.1"], - "classes": ["provider-binding"], - "capabilities": ["squad-work-github/v0.1#binding"], + "conformanceClasses": [], + "implementationRoles": ["producer", "consumer", "validator", "executor"], + "capabilities": [], "manifest": null, - "manifestSchema": null + "manifestSchema": null, + "fixtures": [], + "implementationStatus": "GitHub adapters exist, but deterministic sanitized provider transcripts do not.", + "knownDeviations": ["API-version evidence, precedence, CAS, partial-failure, and drift records are not emitted uniformly."], + "promotionCriteria": ["Add versioned transcripts for simultaneous conditions, stale writes, labels, rate limits, partial failure, and drift."], + "entryPoint": "Read after the neutral work lifecycle." }, { "id": "squad-planning-activation/v0.1", - "maturity": "experimental-normative", + "maturity": "requirements-draft", + "documentClass": "requirements", "spec": "docs/specification/planning-activation-v0.1.md", "dialect": "requirements-only", "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-team-routing/v0.1", "squad-work-lifecycle/v0.1"], - "classes": ["state-machine-executor"], - "capabilities": ["squad-planning-activation/v0.1#transition"], + "conformanceClasses": [], + "implementationRoles": ["producer", "consumer", "validator", "executor"], + "capabilities": [], "manifest": null, - "manifestSchema": null + "manifestSchema": null, + "fixtures": [], + "implementationStatus": "Planning artifacts are observable in gh-aw, but no portable manifest is published.", + "knownDeviations": ["Provider safe-output envelopes and fast-path behavior are implementation-specific."], + "promotionCriteria": ["Add granular and fast-path cases with acceptance revision, partial activation, replay, and dependency failures."], + "entryPoint": "Use for planning ontology coverage only." }, { "id": "squad-ceremony/v0.1", - "maturity": "experimental-normative", + "maturity": "requirements-draft", + "documentClass": "requirements", "spec": "docs/specification/ceremony-v0.1.md", "dialect": "requirements-only", "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-team-routing/v0.1"], - "classes": ["state-machine-executor"], - "capabilities": ["squad-ceremony/v0.1#transition"], + "conformanceClasses": [], + "implementationRoles": ["producer", "consumer", "validator", "executor"], + "capabilities": [], "manifest": null, - "manifestSchema": null + "manifestSchema": null, + "fixtures": [], + "implementationStatus": "Builders preserve trigger strings and schedules; cooldown execution is not proven.", + "knownDeviations": ["Observed triggers include manual, schedule, and event names such as pr-merged; legacy auto is ambiguous."], + "promotionCriteria": ["Add normalized trigger, injected-clock cooldown, duplicate-claim, and output-validation fixtures."], + "entryPoint": "Treat trigger normalization and cooldown as promotion requirements." }, { "id": "squad-coordination-handoff/v0.1", - "maturity": "experimental-normative", + "maturity": "requirements-draft", + "documentClass": "requirements", "spec": "docs/specification/coordination-handoff-v0.1.md", "dialect": "requirements-only", "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-team-routing/v0.1"], - "classes": ["state-machine-executor"], - "capabilities": ["squad-coordination-handoff/v0.1#transition"], + "conformanceClasses": [], + "implementationRoles": ["producer", "consumer", "validator", "executor"], + "capabilities": [], "manifest": null, - "manifestSchema": null + "manifestSchema": null, + "fixtures": [], + "implementationStatus": "Session and handoff implementations are provider-specific.", + "knownDeviations": ["Transport ordering, expiry, and recovery evidence are not portable."], + "promotionCriteria": ["Add message-envelope and transition cases for duplicate, ordering, cancellation, loss, and recovery."], + "entryPoint": "Use for envelope design, not transport conformance." }, { "id": "squad-automation/v0.1", - "maturity": "experimental-normative", + "maturity": "requirements-draft", + "documentClass": "requirements", "spec": "docs/specification/automation-v0.1.md", "dialect": "requirements-only", "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-coordination-handoff/v0.1"], - "classes": ["state-machine-executor"], - "capabilities": ["squad-automation/v0.1#transition"], + "conformanceClasses": [], + "implementationRoles": ["producer", "consumer", "validator", "executor"], + "capabilities": [], "manifest": null, - "manifestSchema": null + "manifestSchema": null, + "fixtures": [], + "implementationStatus": "Local and GitHub schedulers exist without a portable execution manifest.", + "knownDeviations": ["Time-zone, generated-provider, and retry evidence differ by provider."], + "promotionCriteria": ["Add trigger, clock, claim-race, retry, timeout, generation-drift, and redaction fixtures."], + "entryPoint": "Read after coordination requirements." }, { "id": "squad-state-memory-requirements/v0.1", - "maturity": "informative-volatile", + "maturity": "informative-draft", + "documentClass": "informative", "spec": "docs/specification/state-memory-requirements-v0.1.md", "dialect": "requirements-only", "dependencies": ["squad-core/v0.1", "squad-interop/v0.1"], - "classes": [], + "conformanceClasses": [], + "implementationRoles": ["producer", "consumer", "validator", "executor"], "capabilities": [], "manifest": null, - "manifestSchema": null + "manifestSchema": null, + "fixtures": [], + "implementationStatus": "Multiple backends and memory providers exist with different authority and recovery semantics.", + "knownDeviations": ["No backend-neutral CAS, recovery, migration, privacy, or deletion corpus exists."], + "promotionCriteria": ["Meet the independently listed state-authority and memory-classification promotion gates."], + "entryPoint": "Read state authority separately from memory classification." } ] } diff --git a/test-fixtures/spec/suite-v0.1/index.schema.json b/test-fixtures/spec/suite-v0.1/index.schema.json index a3e671fd7..8dffcace7 100644 --- a/test-fixtures/spec/suite-v0.1/index.schema.json +++ b/test-fixtures/spec/suite-v0.1/index.schema.json @@ -68,7 +68,7 @@ }, "capability": { "type": "string", - "pattern": "^squad-[a-z0-9-]+/v(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)#[a-z0-9.-]+$" + "pattern": "^squad-[a-z0-9-]+/v(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)/[a-z0-9-]+$" }, "dialect": { "type": "object", @@ -85,18 +85,38 @@ "required": [ "id", "maturity", + "documentClass", "spec", "dialect", "dependencies", - "classes", + "conformanceClasses", + "implementationRoles", "capabilities", "manifest", - "manifestSchema" + "manifestSchema", + "fixtures", + "implementationStatus", + "knownDeviations", + "promotionCriteria", + "entryPoint" ], "properties": { "id": { "$ref": "#/$defs/profileId" }, "maturity": { - "enum": ["stable-normative", "experimental-normative", "informative-volatile"] + "enum": [ + "published-normative", + "experimental-schema", + "requirements-draft", + "informative-draft" + ] + }, + "documentClass": { + "enum": [ + "normative-profile", + "normative-schema", + "requirements", + "informative" + ] }, "spec": { "$ref": "#/$defs/repoPath" }, "dialect": { "type": "string", "minLength": 1 }, @@ -105,20 +125,31 @@ "items": { "$ref": "#/$defs/profileId" }, "uniqueItems": true }, - "classes": { + "conformanceClasses": { "type": "array", "items": { "enum": [ - "artifact-only", - "validator", - "editor", - "state-machine-executor", - "provider-binding", + "canonical", + "compatible", + "legacy", "runtime" ] }, "uniqueItems": true }, + "implementationRoles": { + "type": "array", + "items": { + "enum": [ + "producer", + "consumer", + "editor", + "validator", + "executor" + ] + }, + "uniqueItems": true + }, "capabilities": { "type": "array", "items": { "$ref": "#/$defs/capability" }, @@ -135,8 +166,60 @@ { "$ref": "#/$defs/repoPath" }, { "type": "null" } ] + }, + "fixtures": { + "type": "array", + "items": { "$ref": "#/$defs/repoPath" }, + "uniqueItems": true + }, + "implementationStatus": { "type": "string", "minLength": 1 }, + "knownDeviations": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + }, + "promotionCriteria": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + }, + "entryPoint": { "type": "string", "minLength": 1 } + }, + "allOf": [ + { + "if": { + "properties": { + "manifest": { "type": "null" } + }, + "required": ["manifest"] + }, + "then": { + "properties": { + "capabilities": { "type": "array", "maxItems": 0 }, + "conformanceClasses": { "type": "array", "maxItems": 0 } + } + } + }, + { + "if": { + "properties": { + "maturity": { + "enum": ["requirements-draft", "informative-draft"] + } + }, + "required": ["maturity"] + }, + "then": { + "properties": { + "documentClass": { + "enum": ["requirements", "informative"] + }, + "capabilities": { "type": "array", "maxItems": 0 }, + "conformanceClasses": { "type": "array", "maxItems": 0 } + } + } } - } + ] } } } diff --git a/test/specification-suite.test.ts b/test/specification-suite.test.ts index a59c6bb17..2a12102ee 100644 --- a/test/specification-suite.test.ts +++ b/test/specification-suite.test.ts @@ -1,17 +1,15 @@ import { readFile } from 'node:fs/promises'; import * as path from 'node:path'; -import Ajv2020, { type AnySchema } from 'ajv/dist/2020.js'; +import Ajv2020, { type AnySchema, type ValidateFunction } from 'ajv/dist/2020.js'; import { describe, expect, it } from 'vitest'; const ROOT = path.resolve(__dirname, '..'); const SPEC_ROOT = path.join(ROOT, 'docs', 'specification'); const FIXTURE_ROOT = path.join(ROOT, 'test-fixtures', 'spec', 'suite-v0.1'); -const normativeDocuments = [ - 'interoperability-conventions-v0.1.md', +const requirementsDocuments = [ 'team-routing-v0.1.md', 'configuration-casting-v0.1.md', - 'governance-review-v0.1.md', 'execution-preferences-v0.1.md', 'planning-activation-v0.1.md', 'ceremony-v0.1.md', @@ -19,6 +17,7 @@ const normativeDocuments = [ 'github-work-lifecycle-binding-v0.1.md', 'coordination-handoff-v0.1.md', 'automation-v0.1.md', + 'state-memory-requirements-v0.1.md', ] as const; async function readJson(file: string): Promise { @@ -33,30 +32,172 @@ function createAjv(): Ajv2020 { return ajv; } +async function evidenceValidator(): Promise { + return createAjv().compile(await readJson('evidence.schema.json') as AnySchema); +} + +function clone(value: T): T { + return structuredClone(value); +} + +function governanceRelationshipErrors(value: { + causationId: string | null; + data: { + reviewerUid: string; + revisionAuthorUid: string; + lockoutUids: string[]; + eligibleRevisionAuthorUids: string[]; + replayDisposition: string; + }; +}): string[] { + const errors: string[] = []; + if (value.data.reviewerUid === value.data.revisionAuthorUid) errors.push('self-review'); + if (!value.data.lockoutUids.includes(value.data.revisionAuthorUid)) errors.push('author-not-locked'); + if (value.data.eligibleRevisionAuthorUids.includes(value.data.revisionAuthorUid)) { + errors.push('locked-author-eligible'); + } + if (value.data.eligibleRevisionAuthorUids.includes(value.data.reviewerUid)) { + errors.push('reviewer-eligible'); + } + if (value.data.replayDisposition === 'duplicate-same-result' && value.causationId === null) { + errors.push('duplicate-without-original-link'); + } + return errors; +} + describe('Squad specification suite v0.1', () => { it.each([ ['index.schema.json', 'index.json'], ['evidence.schema.json', 'governance-rejection.example.json'], ])('validates %s fixtures', async (schemaFile, exampleFile) => { - const ajv = createAjv(); - const validate = ajv.compile(await readJson(schemaFile) as AnySchema); + const validate = createAjv().compile(await readJson(schemaFile) as AnySchema); const example = await readJson(exampleFile); expect(validate(example), JSON.stringify(validate.errors, null, 2)).toBe(true); }); - it('enforces suite index cross-document invariants', async () => { + it('allows neutral observations without an authority assertion', async () => { + const validate = await evidenceValidator(); + const observation = { + specversion: 'squad-interop/v0.1', + schema: 'squad-observation/v0.1', + id: '3f87d0a6-196e-4e07-9abc-77716b5d20b2', + type: 'squad.observation.recorded', + source: 'file:.squad/team.md', + subject: 'record:team-roster', + time: '2026-09-25T14:17:33Z', + actor: { uid: 'observer:local-validator' }, + resource: { uid: 'record:team-roster', revision: 'sha256:abc' }, + correlationId: '9d8b858a-9cad-48d7-8bd5-9c465060182a', + causationId: null, + idempotencyKey: 'observe:team-roster:sha256-abc', + payloadDigest: `sha256:${'a'.repeat(64)}`, + redaction: 'none', + data: { result: 'present' }, + }; + + expect(validate(observation), JSON.stringify(validate.errors, null, 2)).toBe(true); + }); + + it('rejects incomplete authority assertions', async () => { + const validate = await evidenceValidator(); + const example = await readJson('governance-rejection.example.json') as Record; + const invalid = clone(example); + const authority = invalid['authority'] as Record; + delete authority['policyRevision']; + + expect(validate(invalid)).toBe(false); + expect(validate.errors).toEqual( + expect.arrayContaining([expect.objectContaining({ instancePath: '/authority' })]), + ); + }); + + it.each([ + ['CAS expected revision', (value: Record) => { + delete (value['resource'] as Record)['expectedRevision']; + }], + ['replay key', (value: Record) => { + delete value['replayKey']; + }], + ['lockout identities', (value: Record) => { + delete (value['data'] as Record)['lockoutUids']; + }], + ['blocking evidence', (value: Record) => { + (value['data'] as Record)['blockers'] = []; + }], + ['transition states', (value: Record) => { + ((value['data'] as Record)['transition'] as Record)['currentState'] = 'completed'; + }], + ])('rejects governance evidence missing %s', async (_name, mutate) => { + const validate = await evidenceValidator(); + const invalid = clone(await readJson('governance-rejection.example.json') as Record); + mutate(invalid); + + expect(validate(invalid)).toBe(false); + }); + + it('enforces reviewer independence and revision-author eligibility', async () => { + const example = await readJson('governance-rejection.example.json') as { + data: { + reviewerUid: string; + revisionAuthorUid: string; + lockoutUids: string[]; + eligibleRevisionAuthorUids: string[]; + }; + }; + + expect(example.data.reviewerUid).not.toBe(example.data.revisionAuthorUid); + expect(example.data.lockoutUids).toContain(example.data.revisionAuthorUid); + expect(example.data.eligibleRevisionAuthorUids).not.toContain(example.data.revisionAuthorUid); + expect(example.data.eligibleRevisionAuthorUids).not.toContain(example.data.reviewerUid); + expect(governanceRelationshipErrors({ ...example, causationId: null })).toEqual([]); + }); + + it('rejects invalid lockout and replay relationships', async () => { + const example = await readJson('governance-rejection.example.json') as { + replayKey: string; + idempotencyKey: string; + data: { + reviewerUid: string; + revisionAuthorUid: string; + lockoutUids: string[]; + eligibleRevisionAuthorUids: string[]; + replayDisposition: string; + }; + }; + const invalidLockout = clone(example); + invalidLockout.data.lockoutUids = []; + invalidLockout.data.eligibleRevisionAuthorUids = [invalidLockout.data.revisionAuthorUid]; + const invalidReplay = clone(example); + invalidReplay.data.replayDisposition = 'duplicate-same-result'; + + expect(governanceRelationshipErrors({ ...invalidLockout, causationId: null })).toEqual( + expect.arrayContaining(['author-not-locked', 'locked-author-eligible']), + ); + expect(governanceRelationshipErrors({ ...invalidReplay, causationId: null })).toContain( + 'duplicate-without-original-link', + ); + }); + + it('enforces suite index and Charter manifest invariants', async () => { const index = await readJson('index.json') as { dialects: Array<{ id: string }>; sharedSchemas: string[]; profiles: Array<{ id: string; + maturity: string; spec: string; dialect: string; dependencies: string[]; capabilities: string[]; + conformanceClasses: string[]; manifest: string | null; manifestSchema: string | null; + fixtures: string[]; + implementationStatus: string; + knownDeviations: string[]; + promotionCriteria: string[]; + entryPoint: string; }>; }; const profileIds = new Set(index.profiles.map(profile => profile.id)); @@ -66,15 +207,22 @@ describe('Squad specification suite v0.1', () => { expect(profileIds.size).toBe(index.profiles.length); for (const profile of index.profiles) { expect(dialectIds.has(profile.dialect), profile.id).toBe(true); - expect(profile.capabilities.every(capability => capability.startsWith(`${profile.id}#`)), profile.id).toBe(true); expect(profile.dependencies.every(dependency => profileIds.has(dependency)), profile.id).toBe(true); expect(await readFile(path.join(ROOT, profile.spec), 'utf8')).not.toHaveLength(0); - if (profile.manifest !== null) { + expect(profile.implementationStatus, profile.id).not.toHaveLength(0); + expect(profile.entryPoint, profile.id).not.toHaveLength(0); + + if (profile.manifest === null) { + expect(profile.manifestSchema, profile.id).toBeNull(); + expect(profile.capabilities, profile.id).toEqual([]); + expect(profile.conformanceClasses, profile.id).toEqual([]); + expect(profile.promotionCriteria.length, profile.id).toBeGreaterThan(0); + } else { expect(profile.manifestSchema, profile.id).not.toBeNull(); const manifest = JSON.parse(await readFile(path.join(ROOT, profile.manifest), 'utf8')) as { - profile?: string; - schema?: string; - cases?: Array<{ id: string }>; + profile: string; + capabilities: string[]; + cases: Array<{ id: string }>; }; const schema = JSON.parse( await readFile(path.join(ROOT, profile.manifestSchema!), 'utf8'), @@ -82,13 +230,18 @@ describe('Squad specification suite v0.1', () => { const validate = createAjv().compile(schema); expect(validate(manifest), `${profile.id}: ${JSON.stringify(validate.errors, null, 2)}`).toBe(true); - expect(manifest.profile ?? manifest.schema, profile.id).toBe(profile.id); - for (const testCase of manifest.cases ?? []) { - const qualifiedId = `${profile.id}#${testCase.id}`; + expect(manifest.profile).toBe(profile.id); + expect(profile.capabilities).toEqual(manifest.capabilities); + for (const testCase of manifest.cases) { + const qualifiedId = `${profile.id}/${testCase.id}`; expect(caseIds.has(qualifiedId), qualifiedId).toBe(false); caseIds.add(qualifiedId); } } + + for (const fixture of profile.fixtures) { + expect(await readFile(path.join(ROOT, fixture), 'utf8')).not.toHaveLength(0); + } } for (const schema of index.sharedSchemas) { @@ -97,13 +250,13 @@ describe('Squad specification suite v0.1', () => { } }); - it.each(normativeDocuments)('%s exposes falsifiable conformance sections', async (file) => { + it.each(requirementsDocuments)('%s is non-claimable and avoids BCP 14 keywords', async (file) => { const content = await readFile(path.join(SPEC_ROOT, file), 'utf8'); - expect(content).toMatch(/\*\*Maturity:\*\*/); - expect(content).toMatch(/## \d+\. .*Conformance/i); - expect(content).toMatch(/\bMUST\b/); - expect(content).toMatch(/squad-interop\/v0\.1|Interoperability Conventions/); + expect(content).toMatch(/\*\*Maturity:\*\* Requirements draft|\*\*Maturity:\*\* Informative draft/); + expect(content).toMatch(/\*\*Claimable capabilities:\*\* None/); + expect(content).toMatch(/## \d+\. Promotion criteria/i); + expect(content).not.toMatch(/\b(MUST|MUST NOT|SHOULD|SHOULD NOT|MAY|REQUIRED|RECOMMENDED|OPTIONAL)\b/); }); it('keeps volatile provider catalogs and casting aesthetics non-normative', async () => { @@ -118,6 +271,6 @@ describe('Squad specification suite v0.1', () => { expect(preferences).not.toMatch(/gpt-|claude-|gemini-/i); expect(casting).toContain('Casting aesthetics are an'); - expect(casting).toContain('MUST NOT define authority'); + expect(casting).toContain('do not define authority'); }); }); From 453a2945f0cce48e58aebd0621f063d1f35287a4 Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Fri, 25 Sep 2026 08:07:10 -0700 Subject: [PATCH 07/11] fix: close specification suite contracts Part of #2069 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 3a7fb5391975486eebc91bfecb5a624f425bbc6d) --- docs/specification/README.md | 39 +-- docs/specification/artifact-inventory.md | 3 +- .../github-work-lifecycle-binding-v0.1.md | 35 ++- docs/specification/governance-review-v0.1.md | 100 ++++--- .../interoperability-conventions-v0.1.md | 110 +++++--- .../specification/planning-activation-v0.1.md | 6 +- .../spec/suite-v0.1/evidence.schema.json | 141 +++++++++- .../governance-rejection.example.json | 43 +-- test-fixtures/spec/suite-v0.1/index.json | 14 +- .../spec/suite-v0.1/index.schema.json | 3 +- test/specification-suite.test.ts | 261 +++++++++++++++++- 11 files changed, 614 insertions(+), 141 deletions(-) diff --git a/docs/specification/README.md b/docs/specification/README.md index 5d856d1ef..17bd2d490 100644 --- a/docs/specification/README.md +++ b/docs/specification/README.md @@ -9,9 +9,10 @@ **Working-draft provenance:** [PR #2078](https://github.com/bradygaster/squad/pull/2078) contains this suite revision. Its rejected review baseline is [`c03ba950fde071c8ec58a64504fdb7b5cf1206ef`](https://github.com/bradygaster/squad/commit/c03ba950fde071c8ec58a64504fdb7b5cf1206ef). -Revision cycle 2 is authored by `agent-orchestration-dev`; the original -Architect author and rejecting reviewers are excluded from implementation by -the governance lockout. +Revision cycle 2 was authored by `agent-orchestration-dev`; final revision +cycle 3 is authored by `unit-contract-tester`. The original Architect author +and rejecting reviewers are excluded from implementation by the governance +lockout. The source index keeps `publication.revision` and `publication.digest` null while it is a working draft. After review, the exact head SHA is recorded in @@ -26,17 +27,18 @@ index without a self-referential hash that would invalidate the commit. **Foundation profile:** [Squad Charter Profile v0.1](charter-v0.1.md) -This directory maps all major stable or observable Squad surfaces. It -distinguishes the one executable normative profile from experimental schemas, -non-claimable requirements drafts, and informative coverage. +This directory maps all major stable or observable Squad surfaces. Publication +status is independent from maturity: this suite is an unpublished working +draft even where a profile has executable conformance evidence. ## Maturity taxonomy -The suite uses exactly four maturity values: +The suite uses exactly five maturity values: | Maturity | Meaning | Claimable | |---|---|---| -| `published-normative` | Executable manifest and schema with reviewed cases | Yes, only identifiers declared by that manifest | +| `executable-normative-draft` | Normative working draft with an executable manifest and schema | Yes, only identifiers declared by that manifest; not published | +| `experimental-normative-draft` | Normative working draft whose rules are exercised only through dependent profiles or partial evidence | No standalone claims | | `experimental-schema` | Machine-valid schema or fixture, but no complete conformance manifest | No | | `requirements-draft` | Conceptual and behavioral requirements awaiting portable fixtures | No | | `informative-draft` | Coverage, observations, or volatile requirements | No | @@ -60,9 +62,9 @@ deviations are summarized here and expanded in the | ID | Maturity / class | Dependencies | Claimable capabilities | Schema/manifest | Implementation status | Known deviations | Reader entry path | |---|---|---|---|---|---|---|---| -| `squad-core/v0.1` | `requirements-draft` / requirements | None | None | None | Terminology is used by Charter | Canonical JSON and publication rules incomplete | [Core](core-v0.1.md), then Charter | +| `squad-core/v0.1` | `experimental-normative-draft` / normative profile | None | None | None | Normative rules are exercised by Charter; no standalone Core manifest | Canonical JSON and standalone Core cases incomplete | [Core](core-v0.1.md), then Charter | | `squad-interop/v0.1` | `experimental-schema` / normative schema | Core | None | Shared evidence schema; no manifest | Evidence validation is executable | No discovery or replay corpus | [Interoperability](interoperability-conventions-v0.1.md) | -| `squad-charter/v0.1` | `published-normative` / normative profile | Core | `squad-charter/v0.1/parse`, `squad-charter/v0.1/validate`, `squad-charter/v0.1/edit`, `squad-charter/v0.1/legacy-consume`, `squad-charter/v0.1/runtime` | Manifest and schema | Parser, validator, editor, compiler, and cases implemented | None registered | [Charter](charter-v0.1.md) | +| `squad-charter/v0.1` | `executable-normative-draft` / normative profile | Core | `squad-charter/v0.1/parse`, `squad-charter/v0.1/validate`, `squad-charter/v0.1/edit`, `squad-charter/v0.1/legacy-consume`, `squad-charter/v0.1/runtime` | Manifest and schema | Parser, validator, editor, compiler, and cases implemented | None registered | [Charter](charter-v0.1.md) | | `squad-team-routing/v0.1` | `requirements-draft` / requirements | Core, Interop, Charter | None | None | Roster and routing exist in multiple shapes | Fallback and ambiguity differ | [Roster, then routing](team-routing-v0.1.md) | | `squad-configuration-casting/v0.1` | `requirements-draft` / requirements | Core, Interop, Team/Routing | None | None | Configuration, registry, and history implemented | Legacy shapes and runtime-only keys | [Configuration, casting, provenance](configuration-casting-v0.1.md) | | `squad-governance-review/v0.1` | `experimental-schema` / normative schema | Core, Interop, Team/Routing | None | Evidence schema plus rejection fixture; no manifest | Rejection records are machine-valid | Baseline owner self-revision conflict | [Evidence contract, then transitions](governance-review-v0.1.md) | @@ -140,17 +142,18 @@ and the GitHub binding identify their narrower assumptions. An informative precedent does not become a suite requirement merely by being cited. -The existing `test/docs-links.test.ts` command scans `docs/src/content/**` and -the root README, not `docs/specification/**`; there is no existing supported -link-check command for this directory. The suite test verifies every indexed -specification, schema, manifest, and fixture path exists. External URL -availability and section anchors remain review-time checks until the repository -link checker expands its supported scope. +`test/specification-suite.test.ts` deterministically checks every Markdown file +in this directory for relative targets, local heading anchors, and absolute +HTTPS external citations. It also verifies every indexed specification, +schema, manifest, and fixture path. The check is intentionally offline: +external URL availability and remote anchor existence remain review-time or +publication-pipeline checks. ## Versioning policy Profile identifiers use `vMAJOR.MINOR`; mutable state revisions, provider API versions, implementation versions, and the observed portable-bundle `1.0` -version are distinct. Draft documents may change before promotion. Published -normative revisions are immutable except for dated errata; behavior changes +version are distinct. Draft documents may change before promotion. Only a +`publication.status` of `published` denotes an immutable publication; published +normative revisions are immutable except for dated errata, and behavior changes require a new profile version. diff --git a/docs/specification/artifact-inventory.md b/docs/specification/artifact-inventory.md index 0696ece55..78d414aed 100644 --- a/docs/specification/artifact-inventory.md +++ b/docs/specification/artifact-inventory.md @@ -13,7 +13,8 @@ render them beneath `.squad/`. | Surface | Repository evidence | Disposition | Specification | Maturity or promotion rationale | |---|---|---|---|---| | Discovery, negotiation, authority identity, shared evidence, secrets | Core/profile metadata, registry IDs, events, audit records | Experimental shared schema; no claim | Interoperability Conventions v0.1 | Evidence schema exists; discovery and negotiation need a manifest | -| Agent charter | `agents/{id}/charter.md`, compiler, validator, fixtures | Normative profile now | Charter v0.1 | Stable parser and round-trip evidence | +| Core rules | profile identifiers, paths, diagnostics, trust boundary | Experimental normative working draft; no standalone claim | Core v0.1 | Charter depends on and exercises Core rules, but Core has no standalone manifest | +| Agent charter | `agents/{id}/charter.md`, compiler, validator, fixtures | Executable normative working draft | Charter v0.1 | Stable parser and round-trip evidence; publication remains `working-draft` | | Team roster | `team.md`, roster templates, presets | Requirements draft; no claim | Team and Routing v0.1 | Promote independently with roster parsing, mutation, join, and diagnostic cases | | Routing ownership | `routing.md`, routing parser, coordinator tests | Requirements draft; no claim | Team and Routing v0.1 | Promote independently with priority, fallback, ambiguity, stale-write, and resolution cases | | Runtime configuration | `config.json`, config schema and loaders | Requirements draft; no claim | Configuration and Casting v0.1 | Promote independently with precedence and source-evidence cases | diff --git a/docs/specification/github-work-lifecycle-binding-v0.1.md b/docs/specification/github-work-lifecycle-binding-v0.1.md index 50a5ff676..713173768 100644 --- a/docs/specification/github-work-lifecycle-binding-v0.1.md +++ b/docs/specification/github-work-lifecycle-binding-v0.1.md @@ -126,12 +126,35 @@ provider range is revalidated. ## 8. Versioned provider assumptions and references -This draft assumes GitHub REST API version `2022-11-28`, GraphQL schema behavior -observed on 2026-09-25, and `gh` operations that map to those APIs. A conforming -binding must publish its exact supported API versions and observation date. -An unavailable version returns `unsupported-provider-version`; a changed field, -enum, mutation, or ruleset behavior returns `provider-drift` and fails closed -for mutation until revalidated. +This draft assumes GitHub REST API version `2022-11-28`. GraphQL field names and +`gh` transport behavior are informative provider observations only; they are +not normative evidence for this non-claimable binding. A future executable +binding is expected to publish exact supported API versions and observation +dates. An unavailable version is expected to return +`unsupported-provider-version`; a changed field, enum, mutation, or ruleset +behavior is expected to return `provider-drift` and fail closed for mutation +until revalidated. + +The GraphQL field observation was reproduced on 2026-09-25 with this schema +introspection query: + +```graphql +query SquadBindingSchemaObservation { + issue: __type(name: "Issue") { + fields { name } + } + pullRequest: __type(name: "PullRequest") { + fields { name } + } +} +``` + +The relevant field catalogs are +[GitHub GraphQL `Issue`](https://docs.github.com/en/graphql/reference/objects#issue) +and +[GitHub GraphQL `PullRequest`](https://docs.github.com/en/graphql/reference/objects#pullrequest). +This observation establishes reproducibility of field discovery only; it does +not establish snapshot atomicity, transition semantics, or conformance. GitHub API versions and this binding version are independent. Namespaced labels and evidence fields round-trip. Issue bodies and comments are untrusted input. diff --git a/docs/specification/governance-review-v0.1.md b/docs/specification/governance-review-v0.1.md index 8c9f1c9c4..82b90242a 100644 --- a/docs/specification/governance-review-v0.1.md +++ b/docs/specification/governance-review-v0.1.md @@ -24,6 +24,8 @@ repository governance and lacked one machine-readable transition contract. ## 1. Scope and non-goals +**Section status:** Informative overview. + This profile defines accountable ownership, reviewer independence, blocking verdict evidence, rejection lockout, reassignment, escalation, approval, and completion transitions. It does not define code-review style, preferred @@ -31,24 +33,34 @@ feedback wording, repository merge policy, or who is authorized by a provider. ## 2. Roles and vocabulary -The **accountable owner** owns the requirement and final handoff. The -**revision author** produces a reviewed-subject revision. A **blocking reviewer** issues -verdicts. A **revision author** independently produces the next revision after -rejection. A **coordinator** validates transitions. An **authority** resolves -deadlock or policy disputes. +**Section status:** Normative vocabulary for the experimental schema and suite +relationship checks. + +The **accountable owner** owns the requirement and final handoff. The **current +revision author** produced the revision receiving the represented verdict. A +**blocking reviewer** issues verdicts. A **successor revision author** is the +independent author assigned to the next revision after rejection. A +**coordinator** validates transitions. An **authority** resolves deadlock or +policy disputes. ## 3. Evidence record +**Section status:** Experimental normative contract enforced by the shared +schema and suite tests. + Every represented transition MUST use the shared evidence envelope conforming to `test-fixtures/spec/suite-v0.1/evidence.schema.json`. Governance data includes prior and current state, reviewed subject UID and revision, accountable owner, -revision author, independent reviewer, blockers and dispositions, lockout and -eligible-author sets, escalation/deadlock status, replay disposition, gate -result, actor, timestamp, idempotency key, replay key, expected revision, and -result revision. +current revision author, successor revision author, independent reviewer, +blockers and dispositions, lockout and eligible-author sets, +escalation/deadlock status, replay disposition, gate result, actor, timestamp, +idempotency key, replay key, expected revision, and result revision. ## 4. State machine +**Section status:** Experimental normative transition contract enforced by the +schema's transition pairs. + The deterministic states are: `draft -> submitted -> under-review -> approved -> completed` @@ -66,6 +78,9 @@ Completion does not imply provider merge unless a binding says so. ## 5. Blocking verdict contract +**Section status:** Experimental normative rejection contract enforced by +transition conditionals and mutation tests. + A rejection MUST identify review-subject location, violated requirement or invariant, reproducible evidence or concrete failure scenario, and the minimum clearing condition. Preference-only feedback MUST be advisory and MUST NOT trigger @@ -73,58 +88,75 @@ lockout. ## 6. Reviewer independence and lockout state machine +**Section status:** Experimental normative relationship contract enforced by +suite relationship and mutation tests. + The blocking reviewer MUST differ from the current revision author. After a -rejection, the current author is added to the review-subject revision-cycle lockout -set and MUST NOT author, co-author, pair on, or direct the next revision. The -reviewer MAY clarify the blocker but MUST NOT become revision author for the -same cycle. The coordinator MUST select an eligible revision author not in the -lockout set. +rejection, the current revision author is added to the review-subject +revision-cycle lockout set and MUST NOT author, co-author, pair on, or direct +the next revision. The reviewer MAY clarify the blocker but MUST NOT become the +successor revision author for the same cycle. The coordinator MUST select a +successor revision author from the eligible set and outside the lockout set. Lockout is scoped to the review subject and persists until a later revision is approved or an authority records an explicit override. A rejected revision -adds its author to the set. If no eligible author remains, the coordinator MUST -escalate rather than silently readmit a locked-out author. +adds its current revision author to the set. If no eligible successor revision +author remains, the coordinator MUST record a deadlocked escalation rather than +silently readmit a locked-out author. The separately testable lockout states are `clear`, `author-locked`, `revision-assigned`, `revision-submitted`, `cleared`, and `deadlocked`. -This resolves a repository inconsistency: `.squad/governance.md` currently -assigns immediate revision preparation to the accountable owner, while the -reviewer protocol requires independent revision. Portable conformance follows -the stricter auditable rule; implementations using owner self-revision are -nonconforming until they declare a separate compatibility policy. +This contract intentionally differs from repository governance that assigns +immediate revision preparation to the accountable owner. The difference is a +known draft-policy incompatibility, not a published conformance verdict. ## 7. Approval and reassignment +**Section status:** Experimental normative contract enforced by transition +conditionals and mutation tests. + Approval MUST identify the reviewed revision and blocker disposition. A reviewer MUST NOT approve a revision they authored. Reassignment changes the -revision author, not the accountable owner, unless an authority separately -records ownership transfer. +successor revision author, not the accountable owner, unless an authority +separately records ownership transfer. ## 8. Failure, idempotency, and concurrency -Invalid transitions, self-review, locked-out revision authors, missing blocker -evidence, stale reviewed-subject revisions, and duplicate event IDs fail closed. -Repeating an idempotency key returns the original result. Concurrent events use -the prior event ID and reviewed-subject revision as compare-and-swap conditions. +**Section status:** Invalid transitions, self-review, locked-out successor +revision authors, and missing rejection evidence are experimental normative +contracts enforced by the schema or suite tests. Retry, storage, and +concurrency behavior remain informative future-profile requirements. + +Invalid transitions, self-review, locked-out successor revision authors, and +missing blocker evidence MUST fail validation. Future execution profiles should +fail closed for stale reviewed-subject revisions and duplicate event IDs, +return the original result for a repeated idempotency key, and use the prior +event ID plus reviewed-subject revision as compare-and-swap conditions. ## 9. Versioning and extensions -Consumers MUST reject unsupported profile versions and unknown mandatory -states. Namespaced evidence fields are preserved. New advisory event types MAY -be added compatibly; changing transition eligibility requires a new profile. +**Section status:** Informative future-profile requirements. + +Future consumers are expected to reject unsupported profile versions and +unknown mandatory states. Namespaced evidence fields are expected to be +preserved. New advisory event types may be added compatibly; changing +transition eligibility requires a new profile. ## 10. Security, privacy, and authority +**Section status:** Informative security requirements pending dedicated +mutation fixtures. + Governance evidence does not grant repository permissions. Actors and -reviewers MUST be authenticated by the executing environment. Records SHOULD -reference evidence rather than duplicate secrets, private logs, or personal -data. +reviewers are expected to be authenticated by the executing environment. +Records should reference evidence rather than duplicate secrets, private logs, +or personal data. ## 11. Promotion criteria Publish a transition manifest with positive and negative cases for approval, advisory feedback, rejection, lockout, repeated rejection, reassignment, -eligible revision authors, escalation, deadlock, stale CAS, duplicate replay, +eligible successor revision authors, escalation, deadlock, stale CAS, duplicate replay, blocker disposition, and external-check gating. The registered rejection JSON is an example fixture, not a conformance manifest. diff --git a/docs/specification/interoperability-conventions-v0.1.md b/docs/specification/interoperability-conventions-v0.1.md index 0fdd582f5..e9e32851b 100644 --- a/docs/specification/interoperability-conventions-v0.1.md +++ b/docs/specification/interoperability-conventions-v0.1.md @@ -25,12 +25,18 @@ document, authority assertion shape, or replay-retention policy. These conventions define discovery, conformance claims, authority identity, version negotiation, shared evidence, concurrency, duplicate suppression, and -privacy rules reused by every profile and provider binding. Profiles MUST -reference these definitions rather than invent local alternatives. +privacy rules reused by every profile and provider binding. + +**Section status:** Informative overview. Only the explicitly marked schema +contracts in §§4 and 6 are normative in this experimental draft. ## 2. Suite discovery and capability claims -An implementation MUST expose a discovery document or API result containing: +**Section status:** Informative future-profile requirements; no executable +discovery schema or conformance cases are registered. + +A future discovery profile is expected to expose a document or API result +containing: - supported core, convention, profile, and binding identifiers; - exact capability identifiers and operations; @@ -39,23 +45,30 @@ An implementation MUST expose a discovery document or API result containing: - extension namespaces owned or understood; - provider assumptions and implementation-policy boundaries. -Discovery is descriptive, not authorization. Unsupported mandatory versions or -capabilities MUST fail closed. Implementations MUST NOT infer nearest-version -compatibility. +Discovery is descriptive, not authorization. A future executable profile is +expected to fail closed for unsupported mandatory versions or capabilities and +to avoid inferring nearest-version compatibility. ## 3. Stable identity and authority +**Section status:** Informative design requirements except for authority fields +validated by the shared envelope in §4. + Every actor, member, service, and portable resource has an immutable UID scoped by an issuer. Display names, member IDs used as paths, roles, casting personas, and -provider logins are attributes and MUST NOT be treated as the authority UID. +provider logins are attributes rather than the authority UID. Neutral observations identify the actor UID without implying authority. When -an authority assertion is present, it MUST identify action, issuer, -authenticated provider identity, authority basis, resource UID, and policy -revision. Casting or persona changes MUST NOT alter authority. +an authority assertion is present, the executable envelope schema requires +action, issuer, authenticated provider identity, authority basis, resource UID, +and policy revision. Casting or persona changes are not expected to alter +authority. ## 4. Shared event and evidence envelope +**Section status:** Experimental normative schema contract, enforced by +`evidence.schema.json` and `test/specification-suite.test.ts`. + Normative events and evidence MUST conform to `test-fixtures/spec/suite-v0.1/evidence.schema.json`. The envelope carries: @@ -66,51 +79,70 @@ Normative events and evidence MUST conform to - resource UID, revision, expected prior revision, and result revision; - payload digest, redaction declaration, extensions, and typed data. -Profile data belongs under `data`; profiles MUST NOT redefine envelope fields. -Evidence is immutable. Corrections are new events linked by causation. +Profile data MUST appear under `data`; the closed envelope schema rejects +redefined or unknown top-level fields. Evidence immutability and correction +events remain informative future-profile behavior because the schema cannot +enforce storage history. ## 5. Concurrency, retry, and replay -Mutations MUST declare an idempotency key. Operations on revisioned resources -MUST declare the expected prior revision or explicitly state that no revision -precondition is available. A mismatch returns `conflict` without partial -mutation. +**Section status:** Informative future-profile requirements; the envelope +validates represented fields but does not execute retry or replay behavior. -At-least-once delivery is the default. Consumers MUST suppress exact duplicate -event IDs and return the original result for repeated idempotency keys. The -same idempotency key with different payload digest is an error. Replay windows, -leases, and retry limits MUST be declared by the executing capability. +A future transition profile is expected to require mutation idempotency keys +and an expected prior revision, or an explicit declaration that no revision +precondition is available. A mismatch is expected to return `conflict` without +partial mutation. + +At-least-once delivery is the intended default. Future executable cases should +require consumers to suppress exact duplicate event IDs, return the original +result for repeated idempotency keys, reject the same key with a different +payload digest, and declare replay windows, leases, and retry limits. ## 6. Canonical privacy and secrets exclusion -Portable files, records, and evidence MUST NOT contain credentials, access tokens, -private keys, session cookies, secret environment values, hidden prompts, -unredacted sensitive tool output, or provider authorization headers. +**Section status:** The redaction enumeration is an experimental normative +schema contract. Content exclusion and diagnostic behavior are informative +security requirements pending mutation fixtures. Producers MUST declare one of `none`, `metadata-only`, or `content-redacted` in -the envelope's redaction field. Sensitive source material SHOULD be referenced -by an access-controlled URI and digest. Diagnostics MUST identify the excluded -field or class without reproducing secret content. +the envelope's redaction field. Portable files, records, and evidence are +expected to exclude credentials, access tokens, private keys, session cookies, +secret environment values, hidden prompts, unredacted sensitive tool output, +and provider authorization headers. -Profiles MAY impose stricter privacy rules. They MUST NOT weaken this section. +Sensitive source material should be referenced by an access-controlled URI and +digest. Future diagnostics should identify the excluded field or class without +reproducing secret content. Profiles may impose stricter privacy rules and are +not expected to weaken these requirements. ## 7. Extension ownership +**Section status:** Informative future-profile requirements; no extension +round-trip manifest is registered. + Extension keys use a namespace controlled by their owner. The `squad` -namespace is reserved. Discovery MUST identify recognized namespaces and +namespace is reserved. Future discovery records should identify recognized namespaces and whether each is advisory or mandatory. Unknown advisory extensions round-trip; unknown mandatory extensions fail closed. ## 8. Version negotiation and drift +**Section status:** Informative future-profile requirements; no negotiation or +provider-drift manifest is registered. + Negotiation selects exact identifiers and schema versions before semantic -processing. Provider bindings MUST also declare the provider API or behavior -versions assumed. Runtime drift outside the declared range returns -`unsupported-provider-version` or `provider-drift`; it MUST NOT be silently -treated as compatible. +processing. Future provider bindings should declare the provider API or +behavior versions assumed. Runtime drift outside the declared range is expected +to return `unsupported-provider-version` or `provider-drift` rather than be +silently treated as compatible. ## 9. Capability identifiers, classes, and implementation roles +**Section status:** Informative registry design. The current index schema +enforces the capability grammar and prevents entries without manifests from +advertising capabilities or conformance classes. + A claimable capability identifier has exactly this grammar: `squad-/v./`, where each name component uses lowercase ASCII letters, digits, and hyphens. The suite does not define a @@ -130,8 +162,8 @@ The Charter v0.1 registration therefore uses its unchanged `/parse`, Portable case kinds are `positive`, `negative`, `compatibility`, `legacy`, `round-trip`, `mutation`, `transition`, `replay`, `concurrency`, `provider`, -and `security`. Runtime profiles SHOULD promote from requirements-only status -using transition traces with injected clocks and IDs. Provider bindings SHOULD +and `security`. Runtime profiles should promote from requirements-only status +using transition traces with injected clocks and IDs. Provider bindings should reference neutral case IDs and digests rather than duplicate lifecycle rules. Every claimable normative profile requires falsifiable fixtures and an independent @@ -150,9 +182,13 @@ HTTP precedent for idempotent retry analysis. CloudEvents 1.0.2 [§3.1](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#required-attributes), -Kubernetes UID/name separation, OCI media-type negotiation, and in-toto/SLSA -provenance are informative design precedents; they are not imported wholesale -into this schema. +[Kubernetes v1.34 UID/name separation](https://github.com/kubernetes/website/blob/release-1.34/content/en/docs/concepts/overview/working-with-objects/names.md#uids), +[OCI Distribution Specification v1.1.1 content negotiation](https://github.com/opencontainers/distribution-spec/blob/v1.1.1/spec.md#content-negotiation), +[in-toto Attestation Framework v1.0 statement model](https://github.com/in-toto/attestation/blob/v1.0/spec/v1.0/statement.md), +and +[SLSA Provenance v1.0](https://slsa.dev/spec/v1.0/provenance) +are informative design precedents; they are not imported wholesale into this +schema. ## 11. Promotion criteria diff --git a/docs/specification/planning-activation-v0.1.md b/docs/specification/planning-activation-v0.1.md index 3f01161ca..02e30495d 100644 --- a/docs/specification/planning-activation-v0.1.md +++ b/docs/specification/planning-activation-v0.1.md @@ -63,8 +63,10 @@ existence, model output, or a passing validator must not be treated as human approval. PRD prose quality, completeness, and preferred decomposition style are -informative. Artifact identities, revisions, traceability links, dependencies, -acceptance, and activation transitions are normative. +informative. Planning-record identities, revisions, traceability links, +dependencies, acceptance, and activation transitions are future-profile +requirements that become normative only after an executable manifest is +published. ## 5. Activation diff --git a/test-fixtures/spec/suite-v0.1/evidence.schema.json b/test-fixtures/spec/suite-v0.1/evidence.schema.json index 8d7495703..b31ed4a55 100644 --- a/test-fixtures/spec/suite-v0.1/evidence.schema.json +++ b/test-fixtures/spec/suite-v0.1/evidence.schema.json @@ -183,7 +183,8 @@ "transition", "reviewedSubject", "accountableOwnerUid", - "revisionAuthorUid", + "currentRevisionAuthorUid", + "successorRevisionAuthorUid", "reviewerUid", "blockers", "lockoutUids", @@ -290,11 +291,16 @@ } }, "accountableOwnerUid": { "$ref": "#/$defs/uid" }, - "revisionAuthorUid": { "$ref": "#/$defs/uid" }, + "currentRevisionAuthorUid": { "$ref": "#/$defs/uid" }, + "successorRevisionAuthorUid": { + "anyOf": [ + { "$ref": "#/$defs/uid" }, + { "type": "null" } + ] + }, "reviewerUid": { "$ref": "#/$defs/uid" }, "blockers": { "type": "array", - "minItems": 1, "items": { "$ref": "#/$defs/blocker" } }, "lockoutUids": { @@ -304,7 +310,6 @@ }, "eligibleRevisionAuthorUids": { "type": "array", - "minItems": 1, "items": { "$ref": "#/$defs/uid" }, "uniqueItems": true }, @@ -331,7 +336,133 @@ }, "replayDisposition": { "enum": ["original", "duplicate-same-result"] }, "gateResult": { "enum": ["accepted", "rejected", "conflict", "escalated"] } - } + }, + "allOf": [ + { + "if": { + "properties": { + "transition": { + "type": "object", + "properties": { + "currentState": { "const": "rejected" } + }, + "required": ["currentState"] + } + }, + "required": ["transition"] + }, + "then": { + "properties": { + "blockers": { + "type": "array", + "minItems": 1, + "items": { + "allOf": [ + { "$ref": "#/$defs/blocker" }, + { + "type": "object", + "properties": { + "status": { "const": "open" } + } + } + ] + } + }, + "lockoutUids": { "type": "array", "minItems": 1 }, + "successorRevisionAuthorUid": { "$ref": "#/$defs/uid" }, + "gateResult": { "const": "rejected" } + } + } + }, + { + "if": { + "properties": { + "transition": { + "type": "object", + "properties": { + "currentState": { "const": "approved" } + }, + "required": ["currentState"] + } + }, + "required": ["transition"] + }, + "then": { + "properties": { + "blockers": { + "type": "array", + "items": { + "allOf": [ + { "$ref": "#/$defs/blocker" }, + { + "type": "object", + "properties": { + "status": { "enum": ["cleared", "waived"] } + } + } + ] + } + }, + "successorRevisionAuthorUid": { "type": "null" }, + "gateResult": { "const": "accepted" } + } + } + }, + { + "if": { + "properties": { + "transition": { + "type": "object", + "properties": { + "currentState": { "const": "escalated" } + }, + "required": ["currentState"] + } + }, + "required": ["transition"] + }, + "then": { + "properties": { + "gateResult": { "const": "escalated" } + } + } + }, + { + "if": { + "properties": { + "escalation": { + "type": "object", + "properties": { + "deadlocked": { "const": true } + }, + "required": ["deadlocked"] + } + }, + "required": ["escalation"] + }, + "then": { + "properties": { + "transition": { + "type": "object", + "properties": { + "currentState": { "const": "escalated" } + } + }, + "eligibleRevisionAuthorUids": { "type": "array", "maxItems": 0 }, + "successorRevisionAuthorUid": { "type": "null" }, + "escalation": { + "type": "object", + "required": ["reason"], + "properties": { + "status": { "const": "required" }, + "reason": { "type": "string", "minLength": 1 } + } + }, + "gateResult": { "const": "escalated" } + } + } + } + ] } } } diff --git a/test-fixtures/spec/suite-v0.1/governance-rejection.example.json b/test-fixtures/spec/suite-v0.1/governance-rejection.example.json index 25e59e1db..5c21a85cd 100644 --- a/test-fixtures/spec/suite-v0.1/governance-rejection.example.json +++ b/test-fixtures/spec/suite-v0.1/governance-rejection.example.json @@ -3,29 +3,29 @@ "schema": "squad-governance-review/v0.1", "id": "1416ae81-91ef-48b3-b6fa-9b810b86b012", "type": "squad.governance.rejected", - "source": "github:bradygaster/squad", - "subject": "review-subject:change-request/2078", + "source": "https://example.invalid/squad/governance", + "subject": "urn:squad:example:review-subject:change-request-42", "time": "2026-09-25T14:17:33Z", "actor": { - "uid": "squad-member:docs-devrel" + "uid": "urn:squad:example:actor:reviewer" }, "authority": { "action": "review.reject", - "issuer": "squad:bradygaster/squad", - "providerIdentity": "github:reviewer-login", - "basis": "review-gate:publication", - "policyRevision": "governance-2026-09-25" + "issuer": "urn:squad:example:issuer:test-suite", + "providerIdentity": "urn:squad:example:provider-identity:reviewer", + "basis": "urn:squad:example:review-gate:publication", + "policyRevision": "urn:squad:example:policy:governance-v1" }, "resource": { - "uid": "review-subject:change-request/2078", - "revision": "c03ba950fde071c8ec58a64504fdb7b5cf1206ef", - "expectedRevision": "c03ba950fde071c8ec58a64504fdb7b5cf1206ef", - "resultRevision": "c03ba950fde071c8ec58a64504fdb7b5cf1206ef" + "uid": "urn:squad:example:review-subject:change-request-42", + "revision": "urn:squad:example:revision:rejected-1", + "expectedRevision": "urn:squad:example:revision:rejected-1", + "resultRevision": "urn:squad:example:revision:rejected-1" }, "correlationId": "8d1f7875-b855-4996-93fd-fee67c775188", "causationId": null, - "idempotencyKey": "review:2078:c03ba950:docs-devrel", - "replayKey": "github-review:2078:c03ba950:1", + "idempotencyKey": "example-review:change-request-42:revision-1", + "replayKey": "example-provider-review:change-request-42:revision-1", "payloadDigest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "redaction": "metadata-only", "extensions": {}, @@ -35,18 +35,19 @@ "currentState": "rejected" }, "reviewedSubject": { - "uid": "github:bradygaster/squad#2078", - "revision": "c03ba950fde071c8ec58a64504fdb7b5cf1206ef" + "uid": "urn:squad:example:review-subject:change-request-42", + "revision": "urn:squad:example:revision:rejected-1" }, - "accountableOwnerUid": "squad-member:architect", - "revisionAuthorUid": "squad-member:architect", - "reviewerUid": "squad-member:docs-devrel", - "lockoutUids": ["squad-member:architect"], - "eligibleRevisionAuthorUids": ["squad-member:agent-orchestration-dev"], + "accountableOwnerUid": "urn:squad:example:actor:owner", + "currentRevisionAuthorUid": "urn:squad:example:actor:current-author", + "successorRevisionAuthorUid": "urn:squad:example:actor:successor-author", + "reviewerUid": "urn:squad:example:actor:reviewer", + "lockoutUids": ["urn:squad:example:actor:current-author"], + "eligibleRevisionAuthorUids": ["urn:squad:example:actor:successor-author"], "blockers": [ { "code": "publication-maturity-overclaim", - "location": "docs/specification/README.md", + "location": "example/specification.md", "requirement": "Requirements-only drafts are non-claimable", "evidence": "The rejected index advertised capabilities without manifests", "clearCondition": "Downgrade unsupported claims and publish promotion criteria", diff --git a/test-fixtures/spec/suite-v0.1/index.json b/test-fixtures/spec/suite-v0.1/index.json index 11deed3b8..e24edd95c 100644 --- a/test-fixtures/spec/suite-v0.1/index.json +++ b/test-fixtures/spec/suite-v0.1/index.json @@ -10,7 +10,11 @@ "dialects": [ { "id": "charter-v0-1", - "description": "Published Charter v0.1 executable conformance manifest." + "description": "Executable Charter v0.1 working-draft conformance manifest." + }, + { + "id": "normative-draft", + "description": "Normative working draft without standalone claimable capabilities." }, { "id": "suite-v0-1", @@ -27,10 +31,10 @@ "profiles": [ { "id": "squad-core/v0.1", - "maturity": "requirements-draft", - "documentClass": "requirements", + "maturity": "experimental-normative-draft", + "documentClass": "normative-profile", "spec": "docs/specification/core-v0.1.md", - "dialect": "requirements-only", + "dialect": "normative-draft", "dependencies": [], "conformanceClasses": [], "implementationRoles": ["producer", "consumer", "editor", "validator", "executor"], @@ -63,7 +67,7 @@ }, { "id": "squad-charter/v0.1", - "maturity": "published-normative", + "maturity": "executable-normative-draft", "documentClass": "normative-profile", "spec": "docs/specification/charter-v0.1.md", "dialect": "charter-v0-1", diff --git a/test-fixtures/spec/suite-v0.1/index.schema.json b/test-fixtures/spec/suite-v0.1/index.schema.json index 8dffcace7..5fafc562e 100644 --- a/test-fixtures/spec/suite-v0.1/index.schema.json +++ b/test-fixtures/spec/suite-v0.1/index.schema.json @@ -104,7 +104,8 @@ "id": { "$ref": "#/$defs/profileId" }, "maturity": { "enum": [ - "published-normative", + "executable-normative-draft", + "experimental-normative-draft", "experimental-schema", "requirements-draft", "informative-draft" diff --git a/test/specification-suite.test.ts b/test/specification-suite.test.ts index 2a12102ee..4f66054fb 100644 --- a/test/specification-suite.test.ts +++ b/test/specification-suite.test.ts @@ -1,4 +1,4 @@ -import { readFile } from 'node:fs/promises'; +import { access, readFile, readdir } from 'node:fs/promises'; import * as path from 'node:path'; import Ajv2020, { type AnySchema, type ValidateFunction } from 'ajv/dist/2020.js'; import { describe, expect, it } from 'vitest'; @@ -40,25 +40,126 @@ function clone(value: T): T { return structuredClone(value); } +async function markdownFiles(root: string): Promise { + const entries = await readdir(root, { withFileTypes: true }); + const nested = await Promise.all(entries.map(async entry => { + const fullPath = path.join(root, entry.name); + if (entry.isDirectory()) return markdownFiles(fullPath); + return entry.isFile() && entry.name.endsWith('.md') ? [fullPath] : []; + })); + return nested.flat().sort(); +} + +function markdownHeadingAnchors(content: string): Set { + const anchors = new Set(); + const counts = new Map(); + + for (const match of content.matchAll(/^#{1,6}\s+(.+?)\s*#*\s*$/gm)) { + const heading = match[1] + .replace(/`([^`]+)`/g, '$1') + .replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1') + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') + .replace(/<[^>]+>/g, '') + .replace(/[*_~]/g, '') + .trim(); + const base = heading + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^a-z0-9_-]/g, ''); + const count = counts.get(base) ?? 0; + counts.set(base, count + 1); + anchors.add(count === 0 ? base : `${base}-${count}`); + } + + return anchors; +} + +function markdownLinkTargets(content: string): Array<{ line: number; target: string }> { + const links: Array<{ line: number; target: string }> = []; + let fenced = false; + + for (const [index, line] of content.split('\n').entries()) { + if (/^\s*(```|~~~)/.test(line)) { + fenced = !fenced; + continue; + } + if (fenced) continue; + + for (const match of line.matchAll(/!?\[[^\]]*]\(([^)\s]+)(?:\s+"[^"]*")?\)/g)) { + links.push({ line: index + 1, target: match[1].replace(/^<|>$/g, '') }); + } + } + + return links; +} + +function findDependencyCycle( + profiles: Array<{ id: string; dependencies: string[] }>, +): string[] | null { + const dependencies = new Map(profiles.map(profile => [profile.id, profile.dependencies])); + const visited = new Set(); + const active = new Set(); + const stack: string[] = []; + + function visit(profileId: string): string[] | null { + if (active.has(profileId)) { + const cycleStart = stack.indexOf(profileId); + return [...stack.slice(cycleStart), profileId]; + } + if (visited.has(profileId)) return null; + + active.add(profileId); + stack.push(profileId); + for (const dependency of dependencies.get(profileId) ?? []) { + const cycle = visit(dependency); + if (cycle) return cycle; + } + stack.pop(); + active.delete(profileId); + visited.add(profileId); + return null; + } + + for (const profile of profiles) { + const cycle = visit(profile.id); + if (cycle) return cycle; + } + return null; +} + function governanceRelationshipErrors(value: { causationId: string | null; data: { reviewerUid: string; - revisionAuthorUid: string; + currentRevisionAuthorUid: string; + successorRevisionAuthorUid: string | null; lockoutUids: string[]; eligibleRevisionAuthorUids: string[]; replayDisposition: string; }; }): string[] { const errors: string[] = []; - if (value.data.reviewerUid === value.data.revisionAuthorUid) errors.push('self-review'); - if (!value.data.lockoutUids.includes(value.data.revisionAuthorUid)) errors.push('author-not-locked'); - if (value.data.eligibleRevisionAuthorUids.includes(value.data.revisionAuthorUid)) { + if (value.data.reviewerUid === value.data.currentRevisionAuthorUid) errors.push('self-review'); + if (!value.data.lockoutUids.includes(value.data.currentRevisionAuthorUid)) { + errors.push('author-not-locked'); + } + if (value.data.eligibleRevisionAuthorUids.includes(value.data.currentRevisionAuthorUid)) { errors.push('locked-author-eligible'); } if (value.data.eligibleRevisionAuthorUids.includes(value.data.reviewerUid)) { errors.push('reviewer-eligible'); } + if (value.data.successorRevisionAuthorUid !== null) { + if (!value.data.eligibleRevisionAuthorUids.includes(value.data.successorRevisionAuthorUid)) { + errors.push('successor-not-eligible'); + } + if (value.data.lockoutUids.includes(value.data.successorRevisionAuthorUid)) { + errors.push('successor-locked'); + } + if (value.data.successorRevisionAuthorUid === value.data.reviewerUid) { + errors.push('reviewer-is-successor'); + } + } if (value.data.replayDisposition === 'duplicate-same-result' && value.causationId === null) { errors.push('duplicate-without-original-link'); } @@ -99,6 +200,49 @@ describe('Squad specification suite v0.1', () => { expect(validate(observation), JSON.stringify(validate.errors, null, 2)).toBe(true); }); + it('checks every specification Markdown link and local anchor offline', async () => { + const files = await markdownFiles(SPEC_ROOT); + const contentByFile = new Map( + await Promise.all(files.map(async file => [file, await readFile(file, 'utf8')] as const)), + ); + const errors: string[] = []; + + for (const [file, content] of contentByFile) { + for (const link of markdownLinkTargets(content)) { + if (link.target.startsWith('https://')) continue; + if (/^[a-z][a-z0-9+.-]*:/i.test(link.target) || link.target.startsWith('//')) { + errors.push(`${path.relative(ROOT, file)}:${link.line}: external citation is not absolute HTTPS: ${link.target}`); + continue; + } + + const [rawTarget, rawAnchor] = link.target.split('#', 2); + const target = rawTarget.length === 0 + ? file + : path.resolve(path.dirname(file), decodeURIComponent(rawTarget)); + try { + await access(target); + } catch { + errors.push(`${path.relative(ROOT, file)}:${link.line}: missing local target ${rawTarget}`); + continue; + } + + if (rawAnchor !== undefined) { + if (!target.endsWith('.md')) { + errors.push(`${path.relative(ROOT, file)}:${link.line}: local anchor target is not Markdown: ${link.target}`); + continue; + } + const targetContent = contentByFile.get(target) ?? await readFile(target, 'utf8'); + const anchor = decodeURIComponent(rawAnchor).toLowerCase(); + if (!markdownHeadingAnchors(targetContent).has(anchor)) { + errors.push(`${path.relative(ROOT, file)}:${link.line}: missing local anchor #${rawAnchor} in ${rawTarget || path.basename(file)}`); + } + } + } + } + + expect(errors, errors.join('\n')).toEqual([]); + }); + it('rejects incomplete authority assertions', async () => { const validate = await evidenceValidator(); const example = await readJson('governance-rejection.example.json') as Record; @@ -140,16 +284,18 @@ describe('Squad specification suite v0.1', () => { const example = await readJson('governance-rejection.example.json') as { data: { reviewerUid: string; - revisionAuthorUid: string; + currentRevisionAuthorUid: string; + successorRevisionAuthorUid: string; lockoutUids: string[]; eligibleRevisionAuthorUids: string[]; }; }; - expect(example.data.reviewerUid).not.toBe(example.data.revisionAuthorUid); - expect(example.data.lockoutUids).toContain(example.data.revisionAuthorUid); - expect(example.data.eligibleRevisionAuthorUids).not.toContain(example.data.revisionAuthorUid); + expect(example.data.reviewerUid).not.toBe(example.data.currentRevisionAuthorUid); + expect(example.data.lockoutUids).toContain(example.data.currentRevisionAuthorUid); + expect(example.data.eligibleRevisionAuthorUids).not.toContain(example.data.currentRevisionAuthorUid); expect(example.data.eligibleRevisionAuthorUids).not.toContain(example.data.reviewerUid); + expect(example.data.eligibleRevisionAuthorUids).toContain(example.data.successorRevisionAuthorUid); expect(governanceRelationshipErrors({ ...example, causationId: null })).toEqual([]); }); @@ -159,7 +305,8 @@ describe('Squad specification suite v0.1', () => { idempotencyKey: string; data: { reviewerUid: string; - revisionAuthorUid: string; + currentRevisionAuthorUid: string; + successorRevisionAuthorUid: string; lockoutUids: string[]; eligibleRevisionAuthorUids: string[]; replayDisposition: string; @@ -167,7 +314,7 @@ describe('Squad specification suite v0.1', () => { }; const invalidLockout = clone(example); invalidLockout.data.lockoutUids = []; - invalidLockout.data.eligibleRevisionAuthorUids = [invalidLockout.data.revisionAuthorUid]; + invalidLockout.data.eligibleRevisionAuthorUids = [invalidLockout.data.currentRevisionAuthorUid]; const invalidReplay = clone(example); invalidReplay.data.replayDisposition = 'duplicate-same-result'; @@ -179,8 +326,72 @@ describe('Squad specification suite v0.1', () => { ); }); + it('accepts clean approval and deadlock governance transitions', async () => { + const validate = await evidenceValidator(); + const rejection = await readJson('governance-rejection.example.json') as Record; + const approval = clone(rejection); + approval['type'] = 'squad.governance.approved'; + const approvalData = approval['data'] as Record; + approvalData['transition'] = { priorState: 'under-review', currentState: 'approved' }; + approvalData['successorRevisionAuthorUid'] = null; + approvalData['blockers'] = []; + approvalData['lockoutUids'] = []; + approvalData['eligibleRevisionAuthorUids'] = []; + approvalData['gateResult'] = 'accepted'; + + expect(validate(approval), JSON.stringify(validate.errors, null, 2)).toBe(true); + + const deadlock = clone(rejection); + deadlock['type'] = 'squad.governance.escalated'; + const deadlockData = deadlock['data'] as Record; + deadlockData['transition'] = { priorState: 'rejected', currentState: 'escalated' }; + deadlockData['successorRevisionAuthorUid'] = null; + deadlockData['eligibleRevisionAuthorUids'] = []; + deadlockData['escalation'] = { + status: 'required', + deadlocked: true, + authorityUid: null, + reason: 'No eligible successor revision author remains', + }; + deadlockData['gateResult'] = 'escalated'; + + expect(validate(deadlock), JSON.stringify(validate.errors, null, 2)).toBe(true); + }); + + it.each([ + ['rejection without blockers', (data: Record) => { + data['blockers'] = []; + }], + ['rejection without lockout evidence', (data: Record) => { + data['lockoutUids'] = []; + }], + ['approval with an open blocker', (data: Record) => { + data['transition'] = { priorState: 'under-review', currentState: 'approved' }; + data['successorRevisionAuthorUid'] = null; + data['gateResult'] = 'accepted'; + }], + ['deadlock with an eligible successor', (data: Record) => { + data['transition'] = { priorState: 'rejected', currentState: 'escalated' }; + data['successorRevisionAuthorUid'] = null; + data['escalation'] = { + status: 'required', + deadlocked: true, + authorityUid: null, + reason: 'Contradictory eligible-author evidence', + }; + data['gateResult'] = 'escalated'; + }], + ])('rejects %s', async (_name, mutate) => { + const validate = await evidenceValidator(); + const invalid = clone(await readJson('governance-rejection.example.json') as Record); + mutate(invalid['data'] as Record); + + expect(validate(invalid)).toBe(false); + }); + it('enforces suite index and Charter manifest invariants', async () => { const index = await readJson('index.json') as { + publication: { status: string; revision: string | null; digest: string | null }; dialects: Array<{ id: string }>; sharedSchemas: string[]; profiles: Array<{ @@ -204,7 +415,19 @@ describe('Squad specification suite v0.1', () => { const dialectIds = new Set(index.dialects.map(dialect => dialect.id)); const caseIds = new Set(); + expect(index.publication).toEqual({ status: 'working-draft', revision: null, digest: null }); + expect(index.profiles.every(profile => !profile.maturity.startsWith('published'))).toBe(true); + expect(index.profiles.find(profile => profile.id === 'squad-core/v0.1')).toMatchObject({ + maturity: 'experimental-normative-draft', + manifest: null, + capabilities: [], + conformanceClasses: [], + }); + expect(index.profiles.find(profile => profile.id === 'squad-charter/v0.1')).toMatchObject({ + maturity: 'executable-normative-draft', + }); expect(profileIds.size).toBe(index.profiles.length); + expect(findDependencyCycle(index.profiles)).toBeNull(); for (const profile of index.profiles) { expect(dialectIds.has(profile.dialect), profile.id).toBe(true); expect(profile.dependencies.every(dependency => profileIds.has(dependency)), profile.id).toBe(true); @@ -250,6 +473,22 @@ describe('Squad specification suite v0.1', () => { } }); + it('detects a cyclic suite dependency graph', async () => { + const index = await readJson('index.json') as { + profiles: Array<{ id: string; dependencies: string[] }>; + }; + const cyclic = clone(index.profiles); + const core = cyclic.find(profile => profile.id === 'squad-core/v0.1'); + expect(core).toBeDefined(); + core!.dependencies = ['squad-charter/v0.1']; + + expect(findDependencyCycle(cyclic)).toEqual([ + 'squad-core/v0.1', + 'squad-charter/v0.1', + 'squad-core/v0.1', + ]); + }); + it.each(requirementsDocuments)('%s is non-claimable and avoids BCP 14 keywords', async (file) => { const content = await readFile(path.join(SPEC_ROOT, file), 'utf8'); From 37bdf7a75ecf6e346170a7077cb211697bb93037 Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Mon, 28 Sep 2026 09:34:33 -0700 Subject: [PATCH 08/11] docs: align specification claim boundaries Part of #2069 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/specification/README.md | 28 ++--- docs/specification/artifact-inventory.md | 9 +- docs/specification/core-v0.1.md | 17 +++ docs/specification/governance-review-v0.1.md | 118 +++++++++--------- .../interoperability-conventions-v0.1.md | 70 +++++------ test-fixtures/spec/suite-v0.1/index.json | 30 ++--- test/specification-suite.test.ts | 32 +++-- 7 files changed, 167 insertions(+), 137 deletions(-) diff --git a/docs/specification/README.md b/docs/specification/README.md index 17bd2d490..06f7b70af 100644 --- a/docs/specification/README.md +++ b/docs/specification/README.md @@ -6,13 +6,10 @@ **Draft date:** 2026-09-25 -**Working-draft provenance:** [PR #2078](https://github.com/bradygaster/squad/pull/2078) -contains this suite revision. Its rejected review baseline is -[`c03ba950fde071c8ec58a64504fdb7b5cf1206ef`](https://github.com/bradygaster/squad/commit/c03ba950fde071c8ec58a64504fdb7b5cf1206ef). -Revision cycle 2 was authored by `agent-orchestration-dev`; final revision -cycle 3 is authored by `unit-contract-tester`. The original Architect author -and rejecting reviewers are excluded from implementation by the governance -lockout. +**Working-draft provenance:** [PR #2074](https://github.com/bradygaster/squad/pull/2074) +contains the combined Core, Charter, and suite revision. The suite draft +originated in [PR #2078](https://github.com/bradygaster/squad/pull/2078); +the combined branch preserves its source-commit provenance. The source index keeps `publication.revision` and `publication.digest` null while it is a working draft. After review, the exact head SHA is recorded in @@ -29,7 +26,7 @@ index without a self-referential hash that would invalidate the commit. This directory maps all major stable or observable Squad surfaces. Publication status is independent from maturity: this suite is an unpublished working -draft even where a profile has executable conformance evidence. +draft. Charter is the only profile with executable conformance evidence. ## Maturity taxonomy @@ -63,11 +60,11 @@ deviations are summarized here and expanded in the | ID | Maturity / class | Dependencies | Claimable capabilities | Schema/manifest | Implementation status | Known deviations | Reader entry path | |---|---|---|---|---|---|---|---| | `squad-core/v0.1` | `experimental-normative-draft` / normative profile | None | None | None | Normative rules are exercised by Charter; no standalone Core manifest | Canonical JSON and standalone Core cases incomplete | [Core](core-v0.1.md), then Charter | -| `squad-interop/v0.1` | `experimental-schema` / normative schema | Core | None | Shared evidence schema; no manifest | Evidence validation is executable | No discovery or replay corpus | [Interoperability](interoperability-conventions-v0.1.md) | +| `squad-interop/v0.1` | `requirements-draft` / requirements | Core | None | Reference evidence schema; no manifest | Schema and example records inform future profile design | No discovery, relational-invariant, or replay corpus | [Interoperability](interoperability-conventions-v0.1.md) | | `squad-charter/v0.1` | `executable-normative-draft` / normative profile | Core | `squad-charter/v0.1/parse`, `squad-charter/v0.1/validate`, `squad-charter/v0.1/edit`, `squad-charter/v0.1/legacy-consume`, `squad-charter/v0.1/runtime` | Manifest and schema | Parser, validator, editor, compiler, and cases implemented | None registered | [Charter](charter-v0.1.md) | | `squad-team-routing/v0.1` | `requirements-draft` / requirements | Core, Interop, Charter | None | None | Roster and routing exist in multiple shapes | Fallback and ambiguity differ | [Roster, then routing](team-routing-v0.1.md) | | `squad-configuration-casting/v0.1` | `requirements-draft` / requirements | Core, Interop, Team/Routing | None | None | Configuration, registry, and history implemented | Legacy shapes and runtime-only keys | [Configuration, casting, provenance](configuration-casting-v0.1.md) | -| `squad-governance-review/v0.1` | `experimental-schema` / normative schema | Core, Interop, Team/Routing | None | Evidence schema plus rejection fixture; no manifest | Rejection records are machine-valid | Baseline owner self-revision conflict | [Evidence contract, then transitions](governance-review-v0.1.md) | +| `squad-governance-review/v0.1` | `requirements-draft` / requirements | Core, Interop, Team/Routing | None | Reference schema and rejection fixture; no manifest | TypeScript checks exercise implementation expectations only | No portable relational-invariant or transition corpus | [Evidence shape, then promotion criteria](governance-review-v0.1.md) | | `squad-execution-preferences/v0.1` | `requirements-draft` / requirements | Core, Interop, Charter, Configuration/Casting | None | None | Runtime selection exists | Provider catalogs and fallback are policy | [Execution preferences](execution-preferences-v0.1.md) | | `squad-planning-activation/v0.1` | `requirements-draft` / requirements | Core, Interop, Team/Routing, Work Lifecycle | None | None | gh-aw planning records exist | Safe-output and limits are provider-specific | [Planning and activation](planning-activation-v0.1.md) | | `squad-ceremony/v0.1` | `requirements-draft` / requirements | Core, Interop, Team/Routing | None | None | Builder preserves triggers and schedules | Event names, `auto`, and cooldown are unstable | [Ceremonies](ceremony-v0.1.md) | @@ -80,9 +77,9 @@ deviations are summarized here and expanded in the The machine-readable registry is [`test-fixtures/spec/suite-v0.1/index.json`](../../test-fixtures/spec/suite-v0.1/index.json). -The governance JSON is registered as a fixture, not a conformance manifest. -Charter v0.1's manifest, schema, case IDs, and capability identifiers remain -unchanged. +The governance JSON is a reference fixture, not portable conformance evidence. +Charter v0.1 is the only registered profile with a manifest, conformance +classes, and capability identifiers. ## Textual dependency adjacency and DAG @@ -111,7 +108,7 @@ DAG reading order: ```text Core ├── Charter (only claimable profile) -├── Interop shared schema +├── Interop requirements and reference schema │ ├── Team/Routing │ │ ├── Configuration/Casting │ │ │ └── Execution Preferences @@ -128,7 +125,8 @@ Core There is no dependency cycle. Provider-neutral requirements precede the GitHub binding. State authority and memory classification remain separate promotion -tracks. +tracks. This composition diagram is informative: dependency edges do not confer +transitive capabilities or conformance claims. ## External reference policy diff --git a/docs/specification/artifact-inventory.md b/docs/specification/artifact-inventory.md index 78d414aed..ce4e215c3 100644 --- a/docs/specification/artifact-inventory.md +++ b/docs/specification/artifact-inventory.md @@ -12,7 +12,7 @@ render them beneath `.squad/`. | Surface | Repository evidence | Disposition | Specification | Maturity or promotion rationale | |---|---|---|---|---| -| Discovery, negotiation, authority identity, shared evidence, secrets | Core/profile metadata, registry IDs, events, audit records | Experimental shared schema; no claim | Interoperability Conventions v0.1 | Evidence schema exists; discovery and negotiation need a manifest | +| Discovery, negotiation, authority identity, shared evidence, secrets | Core/profile metadata, registry IDs, events, audit records | Requirements draft with a reference schema; no claim | Interoperability Conventions v0.1 | Discovery, relational invariants, and negotiation need portable manifests and cases | | Core rules | profile identifiers, paths, diagnostics, trust boundary | Experimental normative working draft; no standalone claim | Core v0.1 | Charter depends on and exercises Core rules, but Core has no standalone manifest | | Agent charter | `agents/{id}/charter.md`, compiler, validator, fixtures | Executable normative working draft | Charter v0.1 | Stable parser and round-trip evidence; publication remains `working-draft` | | Team roster | `team.md`, roster templates, presets | Requirements draft; no claim | Team and Routing v0.1 | Promote independently with roster parsing, mutation, join, and diagnostic cases | @@ -20,7 +20,7 @@ render them beneath `.squad/`. | Runtime configuration | `config.json`, config schema and loaders | Requirements draft; no claim | Configuration and Casting v0.1 | Promote independently with precedence and source-evidence cases | | Casting identity | `casting/registry.json`, `policy.json` | Requirements draft; no claim | Configuration and Casting v0.1 | Promote independently with identity, recast, join, and migration cases | | Casting provenance | `casting/history.json` | Requirements draft; no claim | Configuration and Casting v0.1 | Promote independently with append-only history, revision, replay, and CAS cases | -| Governance and review | `governance.md`, reviewer protocol, PR requirements | Experimental evidence schema; no claim | Governance and Review v0.1 | Add complete transition manifest beyond the representative rejection fixture | +| Governance and review | `governance.md`, reviewer protocol, PR requirements | Requirements draft with reference checks; no claim | Governance and Review v0.1 | Add language-neutral relational invariants and a complete transition manifest | | Execution preferences | charters, config, model selector, lifecycle manager | Requirements draft; no claim | Execution Preferences v0.1 | Add provider-neutral resolution cases and loss reporting | | PRD intake and planning | `prd-intake.md`, planning ontology, gh-aw workflows | Requirements draft; no claim | Planning and Activation v0.1 | Add portable planning-record and activation manifest | | Ceremonies | `ceremonies.md`, ceremony reference, retro workflow | Requirements draft; no claim | Ceremony v0.1 | Normalize event vocabulary and prove cooldown with injected clocks | @@ -81,8 +81,9 @@ is equally mature. The next promotion sequence is: **Recorded:** 2026-09-25 - Current `.squad/governance.md` permits accountable-owner revision after a - rejection; the reviewer protocol and Governance Profile require independent - revision-author lockout. + rejection; the reviewer protocol and Governance requirements draft propose + independent revision-author lockout. That relationship is not yet portable + conformance behavior. - Current model selectors contain vendor names and fallback lists. Those are implementation policy and do not conform as portable preference vocabulary. - Current ceremony conditions are natural language. They are compatible input, diff --git a/docs/specification/core-v0.1.md b/docs/specification/core-v0.1.md index ffe5bd488..83d1d6e0e 100644 --- a/docs/specification/core-v0.1.md +++ b/docs/specification/core-v0.1.md @@ -46,6 +46,17 @@ appear in all capitals, as shown here. ## 3. Terms and conformance classes +- **Squad:** a selected Squad root and the artifacts interpreted under + explicitly identified profiles. The term does not imply support for every + known profile. +- **Implementation:** software that performs one or more declared profile + operations. +- **Profile:** an independently versioned contract for a defined artifact or + evidence surface. +- **Capability:** one exact `/` identifier. +- **Conformance claim:** a statement binding an implementation and version to + an exact profile revision, capability identifiers, applicable conformance + class, and evidence revision. - **Squad root:** the `.squad/` directory selected by an implementation. - **Artifact:** a file or directory beneath the Squad root described by a profile. @@ -59,6 +70,12 @@ An implementation MUST declare exact operational capability identifiers. Supporting one profile operation MUST NOT be represented as support for every operation in that profile or for every Squad artifact. +### 3.1 Informative composition guidance + +Profile dependencies establish an interpretation order. They do not confer +transitive capabilities or conformance: an implementation claiming a dependent +profile still declares and evidences each supported capability independently. + ## 4. Root discovery, paths, and containment An implementation MUST resolve one Squad root before resolving artifact paths. diff --git a/docs/specification/governance-review-v0.1.md b/docs/specification/governance-review-v0.1.md index 82b90242a..deefa4300 100644 --- a/docs/specification/governance-review-v0.1.md +++ b/docs/specification/governance-review-v0.1.md @@ -2,17 +2,17 @@ **Status:** Working Draft -**Document class:** Normative experimental evidence schema; transition profile -requirements remain non-claimable +**Document class:** Non-normative requirements draft **Profile identifier:** `squad-governance-review/v0.1` -**Maturity:** Experimental schema; non-claimable +**Maturity:** Requirements draft; non-claimable **Claimable capabilities:** None -**Implementation status:** The shared evidence schema validates representative -governance transitions. No executable transition manifest is registered. +**Implementation status:** A reference evidence schema, example record, and +TypeScript implementation checks exist. They do not establish portable +governance conformance. **Known deviations:** The rejected baseline allowed owner self-revision in repository governance and lacked one machine-readable transition contract. @@ -26,15 +26,16 @@ repository governance and lacked one machine-readable transition contract. **Section status:** Informative overview. -This profile defines accountable ownership, reviewer independence, blocking -verdict evidence, rejection lockout, reassignment, escalation, approval, and -completion transitions. It does not define code-review style, preferred -feedback wording, repository merge policy, or who is authorized by a provider. +This draft collects proposed accountable ownership, reviewer independence, +blocking-verdict evidence, rejection lockout, reassignment, escalation, +approval, and completion requirements. It does not define code-review style, +preferred feedback wording, repository merge policy, or who is authorized by a +provider. ## 2. Roles and vocabulary -**Section status:** Normative vocabulary for the experimental schema and suite -relationship checks. +**Section status:** Informative vocabulary used by the reference schema and +implementation checks. The **accountable owner** owns the requirement and final handoff. The **current revision author** produced the revision receiving the represented verdict. A @@ -45,21 +46,21 @@ policy disputes. ## 3. Evidence record -**Section status:** Experimental normative contract enforced by the shared -schema and suite tests. +**Section status:** Informative reference shape. -Every represented transition MUST use the shared evidence envelope conforming to -`test-fixtures/spec/suite-v0.1/evidence.schema.json`. Governance data includes -prior and current state, reviewed subject UID and revision, accountable owner, -current revision author, successor revision author, independent reviewer, -blockers and dispositions, lockout and eligible-author sets, +The reference fixture uses the shared evidence envelope in +`test-fixtures/spec/suite-v0.1/evidence.schema.json`. Its governance data +represents prior and current state, reviewed subject UID and revision, +accountable owner, current revision author, successor revision author, +reviewer, blockers and dispositions, lockout and eligible-author sets, escalation/deadlock status, replay disposition, gate result, actor, timestamp, -idempotency key, replay key, expected revision, and result revision. +idempotency key, replay key, expected revision, and result revision. Schema +acceptance proves only that represented fields match this draft shape. ## 4. State machine -**Section status:** Experimental normative transition contract enforced by the -schema's transition pairs. +**Section status:** Informative transition design exercised by reference schema +checks. The deterministic states are: @@ -73,36 +74,36 @@ Escalation uses: `rejected|reassignment-required -> escalated -> dispositioned` -Only an approved review subject with applicable external checks may become completed. -Completion does not imply provider merge unless a binding says so. +The intended design allows only an approved review subject with applicable +external checks to become completed. Completion does not imply provider merge +unless a future binding says so. ## 5. Blocking verdict contract -**Section status:** Experimental normative rejection contract enforced by -transition conditionals and mutation tests. +**Section status:** Informative promotion requirements. -A rejection MUST identify review-subject location, violated requirement or invariant, -reproducible evidence or concrete failure scenario, and the minimum clearing -condition. Preference-only feedback MUST be advisory and MUST NOT trigger -lockout. +A future portable rejection contract is expected to identify review-subject +location, violated requirement or invariant, reproducible evidence or concrete +failure scenario, and the minimum clearing condition. Preference-only feedback +is expected to remain advisory and not trigger lockout. ## 6. Reviewer independence and lockout state machine -**Section status:** Experimental normative relationship contract enforced by -suite relationship and mutation tests. +**Section status:** Informative relationship requirements. Current TypeScript +checks are reference-implementation checks, not portable conformance evidence. -The blocking reviewer MUST differ from the current revision author. After a -rejection, the current revision author is added to the review-subject -revision-cycle lockout set and MUST NOT author, co-author, pair on, or direct -the next revision. The reviewer MAY clarify the blocker but MUST NOT become the -successor revision author for the same cycle. The coordinator MUST select a -successor revision author from the eligible set and outside the lockout set. +The proposed design keeps the blocking reviewer distinct from the current +revision author. After a rejection, it adds the current revision author to the +review-subject revision-cycle lockout set and excludes that author from +authoring, co-authoring, pairing on, or directing the next revision. It also +excludes the reviewer from becoming the successor revision author and selects a +successor from the eligible set outside the lockout set. Lockout is scoped to the review subject and persists until a later revision is approved or an authority records an explicit override. A rejected revision adds its current revision author to the set. If no eligible successor revision -author remains, the coordinator MUST record a deadlocked escalation rather than -silently readmit a locked-out author. +author remains, the proposed coordinator behavior records a deadlocked +escalation rather than silently readmitting a locked-out author. The separately testable lockout states are `clear`, `author-locked`, `revision-assigned`, `revision-submitted`, `cleared`, and `deadlocked`. @@ -113,26 +114,23 @@ known draft-policy incompatibility, not a published conformance verdict. ## 7. Approval and reassignment -**Section status:** Experimental normative contract enforced by transition -conditionals and mutation tests. +**Section status:** Informative promotion requirements. -Approval MUST identify the reviewed revision and blocker disposition. A -reviewer MUST NOT approve a revision they authored. Reassignment changes the -successor revision author, not the accountable owner, unless an authority -separately records ownership transfer. +A future portable approval contract is expected to identify the reviewed +revision and blocker disposition, reject self-approval, and distinguish +successor reassignment from accountable ownership transfer. ## 8. Failure, idempotency, and concurrency -**Section status:** Invalid transitions, self-review, locked-out successor -revision authors, and missing rejection evidence are experimental normative -contracts enforced by the schema or suite tests. Retry, storage, and -concurrency behavior remain informative future-profile requirements. +**Section status:** Informative promotion requirements. Current schema and +TypeScript checks cover selected shapes and relationships only. -Invalid transitions, self-review, locked-out successor revision authors, and -missing blocker evidence MUST fail validation. Future execution profiles should -fail closed for stale reviewed-subject revisions and duplicate event IDs, -return the original result for a repeated idempotency key, and use the prior -event ID plus reviewed-subject revision as compare-and-swap conditions. +A future portable validator is expected to reject invalid transitions, +self-review, locked-out successor revision authors, and missing blocker +evidence. Future execution profiles should fail closed for stale +reviewed-subject revisions and duplicate event IDs, return the original result +for a repeated idempotency key, and use the prior event ID plus reviewed-subject +revision as compare-and-swap conditions. ## 9. Versioning and extensions @@ -155,8 +153,10 @@ or personal data. ## 11. Promotion criteria -Publish a transition manifest with positive and negative cases for approval, -advisory feedback, rejection, lockout, repeated rejection, reassignment, -eligible successor revision authors, escalation, deadlock, stale CAS, duplicate replay, -blocker disposition, and external-check gating. The registered rejection JSON -is an example fixture, not a conformance manifest. +Define language-neutral relational invariants and stable diagnostic codes, then +publish a transition manifest with positive and negative cases for approval, +advisory feedback, rejection, reviewer independence, lockout, repeated +rejection, successor eligibility, reassignment, escalation, deadlock, stale +CAS, replay links, blocker disposition, and external-check gating. The current +rejection JSON and TypeScript relationship checks are reference artifacts, not +portable conformance evidence. diff --git a/docs/specification/interoperability-conventions-v0.1.md b/docs/specification/interoperability-conventions-v0.1.md index e9e32851b..d9df95713 100644 --- a/docs/specification/interoperability-conventions-v0.1.md +++ b/docs/specification/interoperability-conventions-v0.1.md @@ -4,17 +4,16 @@ **Status:** Working Draft -**Document class:** Normative shared-schema draft +**Document class:** Non-normative requirements draft -**Capability identifier:** `squad-interop/v0.1` +**Profile identifier:** `squad-interop/v0.1` -**Maturity:** Experimental schema; non-claimable +**Maturity:** Requirements draft; non-claimable **Claimable capabilities:** None -**Implementation status:** The shared evidence schema is executable. Discovery, -negotiation, retention, and cross-implementation conformance manifests are not -published. +**Implementation status:** A shared evidence schema and reference checks exist, +but no portable manifest establishes interoperability conformance. **Known deviations:** Current implementations do not expose one discovery document, authority assertion shape, or replay-retention policy. @@ -23,12 +22,12 @@ document, authority assertion shape, or replay-retention policy. ## 1. Scope -These conventions define discovery, conformance claims, authority identity, -version negotiation, shared evidence, concurrency, duplicate suppression, and -privacy rules reused by every profile and provider binding. +These conventions collect proposed discovery, conformance-claim, authority, +version-negotiation, evidence, concurrency, duplicate-suppression, and privacy +requirements for future profiles and provider bindings. -**Section status:** Informative overview. Only the explicitly marked schema -contracts in §§4 and 6 are normative in this experimental draft. +**Section status:** Informative overview. Every section is non-claimable until +portable manifests and cases are registered. ## 2. Suite discovery and capability claims @@ -51,26 +50,26 @@ to avoid inferring nearest-version compatibility. ## 3. Stable identity and authority -**Section status:** Informative design requirements except for authority fields -validated by the shared envelope in §4. +**Section status:** Informative design requirements. Every actor, member, service, and portable resource has an immutable UID scoped by an issuer. Display names, member IDs used as paths, roles, casting personas, and provider logins are attributes rather than the authority UID. -Neutral observations identify the actor UID without implying authority. When -an authority assertion is present, the executable envelope schema requires -action, issuer, authenticated provider identity, authority basis, resource UID, -and policy revision. Casting or persona changes are not expected to alter -authority. +Neutral observations identify the actor UID without implying authority. The +reference envelope schema checks represented authority fields such as action, +issuer, authenticated provider identity, authority basis, resource UID, and +policy revision. Passing that shape check is not an authority or conformance +claim. Casting or persona changes are not expected to alter authority. ## 4. Shared event and evidence envelope -**Section status:** Experimental normative schema contract, enforced by -`evidence.schema.json` and `test/specification-suite.test.ts`. +**Section status:** Informative reference shape exercised by +`evidence.schema.json` and implementation tests. -Normative events and evidence MUST conform to -`test-fixtures/spec/suite-v0.1/evidence.schema.json`. The envelope carries: +Future portable events and evidence are expected to use a language-neutral +envelope based on `test-fixtures/spec/suite-v0.1/evidence.schema.json`. The +current reference shape carries: - specification and schema identifiers; - event ID, type, source, subject, and time; @@ -79,10 +78,9 @@ Normative events and evidence MUST conform to - resource UID, revision, expected prior revision, and result revision; - payload digest, redaction declaration, extensions, and typed data. -Profile data MUST appear under `data`; the closed envelope schema rejects -redefined or unknown top-level fields. Evidence immutability and correction -events remain informative future-profile behavior because the schema cannot -enforce storage history. +The reference schema places profile data under `data` and rejects redefined or +unknown top-level fields. These syntax checks do not establish relational, +storage, replay, or execution semantics. ## 5. Concurrency, retry, and replay @@ -101,15 +99,14 @@ payload digest, and declare replay windows, leases, and retry limits. ## 6. Canonical privacy and secrets exclusion -**Section status:** The redaction enumeration is an experimental normative -schema contract. Content exclusion and diagnostic behavior are informative -security requirements pending mutation fixtures. +**Section status:** Informative security requirements and reference schema +shape pending portable mutation fixtures. -Producers MUST declare one of `none`, `metadata-only`, or `content-redacted` in -the envelope's redaction field. Portable files, records, and evidence are -expected to exclude credentials, access tokens, private keys, session cookies, -secret environment values, hidden prompts, unredacted sensitive tool output, -and provider authorization headers. +The reference envelope represents redaction as `none`, `metadata-only`, or +`content-redacted`. Future portable files, records, and evidence are expected +to exclude credentials, access tokens, private keys, session cookies, secret +environment values, hidden prompts, unredacted sensitive tool output, and +provider authorization headers. Sensitive source material should be referenced by an access-controlled URI and digest. Future diagnostics should identify the excluded field or class without @@ -172,7 +169,7 @@ authority conformance. ## 10. Versioned references and precedents -The evidence envelope normatively uses +The evidence envelope design references [JSON Schema 2020-12 Core §8](https://json-schema.org/draft/2020-12/json-schema-core#section-8) for identifiers and references, [RFC 3339 §5.6](https://www.rfc-editor.org/rfc/rfc3339#section-5.6) for @@ -196,5 +193,4 @@ Publish a manifest with neutral-observation, complete and incomplete authority, privacy/redaction, expected/result revision, duplicate event, repeated idempotency key, conflicting payload digest, replay, unsupported version, and extension cases. Discovery and negotiation require a separate executable -document or API-result schema before this dependency can advertise a -capability. +document or API-result schema before this requirements draft can advertise a capability. diff --git a/test-fixtures/spec/suite-v0.1/index.json b/test-fixtures/spec/suite-v0.1/index.json index e24edd95c..20b5a1292 100644 --- a/test-fixtures/spec/suite-v0.1/index.json +++ b/test-fixtures/spec/suite-v0.1/index.json @@ -18,7 +18,7 @@ }, { "id": "suite-v0-1", - "description": "Experimental shared schemas and explicitly registered fixtures." + "description": "Reference shared schemas and fixtures with no conformance claim." }, { "id": "requirements-only", @@ -49,10 +49,10 @@ }, { "id": "squad-interop/v0.1", - "maturity": "experimental-schema", - "documentClass": "normative-schema", + "maturity": "requirements-draft", + "documentClass": "requirements", "spec": "docs/specification/interoperability-conventions-v0.1.md", - "dialect": "suite-v0-1", + "dialect": "requirements-only", "dependencies": ["squad-core/v0.1"], "conformanceClasses": [], "implementationRoles": ["producer", "consumer", "validator", "executor"], @@ -60,10 +60,10 @@ "manifest": null, "manifestSchema": null, "fixtures": [], - "implementationStatus": "The shared evidence schema is executable; discovery and negotiation are requirements only.", - "knownDeviations": ["No discovery manifest or cross-implementation evidence corpus exists."], - "promotionCriteria": ["Add a manifest with neutral observation, authority, replay, CAS, and privacy cases."], - "entryPoint": "Read after Core when implementing shared evidence." + "implementationStatus": "A shared evidence schema and reference checks exist, but no portable interoperability manifest is registered.", + "knownDeviations": ["No discovery manifest, relational-invariant catalog, or cross-implementation evidence corpus exists."], + "promotionCriteria": ["Add portable manifests with neutral observation, authority, replay, CAS, privacy, and relational-invariant cases."], + "entryPoint": "Use as requirements and reference shapes after Core." }, { "id": "squad-charter/v0.1", @@ -127,10 +127,10 @@ }, { "id": "squad-governance-review/v0.1", - "maturity": "experimental-schema", - "documentClass": "normative-schema", + "maturity": "requirements-draft", + "documentClass": "requirements", "spec": "docs/specification/governance-review-v0.1.md", - "dialect": "suite-v0-1", + "dialect": "requirements-only", "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-team-routing/v0.1"], "conformanceClasses": [], "implementationRoles": ["producer", "consumer", "validator", "executor"], @@ -140,10 +140,10 @@ "fixtures": [ "test-fixtures/spec/suite-v0.1/governance-rejection.example.json" ], - "implementationStatus": "The evidence schema and representative rejection fixture are executable; no transition manifest exists.", - "knownDeviations": ["Repository governance previously permitted immediate owner self-revision."], - "promotionCriteria": ["Add a transition manifest covering approval, lockout, replay, CAS, blockers, escalation, and deadlock."], - "entryPoint": "Use the evidence contract for records; treat transition prose as draft requirements." + "implementationStatus": "A reference evidence schema, rejection fixture, and TypeScript checks exist; they establish no portable governance claim.", + "knownDeviations": ["Relational invariants are implemented only as TypeScript reference checks, and repository governance previously permitted immediate owner self-revision."], + "promotionCriteria": ["Define language-neutral relational invariants and add a transition manifest covering approval, reviewer independence, lockout, successor eligibility, replay links, CAS, blockers, escalation, and deadlock."], + "entryPoint": "Use as non-claimable requirements and reference shapes." }, { "id": "squad-execution-preferences/v0.1", diff --git a/test/specification-suite.test.ts b/test/specification-suite.test.ts index 4f66054fb..43ccd3540 100644 --- a/test/specification-suite.test.ts +++ b/test/specification-suite.test.ts @@ -127,7 +127,7 @@ function findDependencyCycle( return null; } -function governanceRelationshipErrors(value: { +function referenceGovernanceRelationshipErrors(value: { causationId: string | null; data: { reviewerUid: string; @@ -170,7 +170,7 @@ describe('Squad specification suite v0.1', () => { it.each([ ['index.schema.json', 'index.json'], ['evidence.schema.json', 'governance-rejection.example.json'], - ])('validates %s fixtures', async (schemaFile, exampleFile) => { + ])('validates reference %s fixtures', async (schemaFile, exampleFile) => { const validate = createAjv().compile(await readJson(schemaFile) as AnySchema); const example = await readJson(exampleFile); @@ -280,7 +280,7 @@ describe('Squad specification suite v0.1', () => { expect(validate(invalid)).toBe(false); }); - it('enforces reviewer independence and revision-author eligibility', async () => { + it('checks reviewer independence and revision-author eligibility in the reference implementation', async () => { const example = await readJson('governance-rejection.example.json') as { data: { reviewerUid: string; @@ -296,10 +296,10 @@ describe('Squad specification suite v0.1', () => { expect(example.data.eligibleRevisionAuthorUids).not.toContain(example.data.currentRevisionAuthorUid); expect(example.data.eligibleRevisionAuthorUids).not.toContain(example.data.reviewerUid); expect(example.data.eligibleRevisionAuthorUids).toContain(example.data.successorRevisionAuthorUid); - expect(governanceRelationshipErrors({ ...example, causationId: null })).toEqual([]); + expect(referenceGovernanceRelationshipErrors({ ...example, causationId: null })).toEqual([]); }); - it('rejects invalid lockout and replay relationships', async () => { + it('detects invalid lockout and replay relationships in the reference implementation', async () => { const example = await readJson('governance-rejection.example.json') as { replayKey: string; idempotencyKey: string; @@ -318,10 +318,10 @@ describe('Squad specification suite v0.1', () => { const invalidReplay = clone(example); invalidReplay.data.replayDisposition = 'duplicate-same-result'; - expect(governanceRelationshipErrors({ ...invalidLockout, causationId: null })).toEqual( + expect(referenceGovernanceRelationshipErrors({ ...invalidLockout, causationId: null })).toEqual( expect.arrayContaining(['author-not-locked', 'locked-author-eligible']), ); - expect(governanceRelationshipErrors({ ...invalidReplay, causationId: null })).toContain( + expect(referenceGovernanceRelationshipErrors({ ...invalidReplay, causationId: null })).toContain( 'duplicate-without-original-link', ); }); @@ -426,6 +426,24 @@ describe('Squad specification suite v0.1', () => { expect(index.profiles.find(profile => profile.id === 'squad-charter/v0.1')).toMatchObject({ maturity: 'executable-normative-draft', }); + const claimableProfiles = index.profiles.filter(profile => + profile.manifest !== null || + profile.capabilities.length > 0 || + profile.conformanceClasses.length > 0 + ); + expect(claimableProfiles.map(profile => profile.id)).toEqual(['squad-charter/v0.1']); + expect(index.profiles.find(profile => profile.id === 'squad-interop/v0.1')).toMatchObject({ + maturity: 'requirements-draft', + manifest: null, + capabilities: [], + conformanceClasses: [], + }); + expect(index.profiles.find(profile => profile.id === 'squad-governance-review/v0.1')).toMatchObject({ + maturity: 'requirements-draft', + manifest: null, + capabilities: [], + conformanceClasses: [], + }); expect(profileIds.size).toBe(index.profiles.length); expect(findDependencyCycle(index.profiles)).toBeNull(); for (const profile of index.profiles) { From dd4565a482d1d0e88d2065c325eaaa5e2810edf6 Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Mon, 28 Sep 2026 11:16:33 -0700 Subject: [PATCH 09/11] fix: bind Charter claims to immutable revisions Part of #2069 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/specification/README.md | 15 ++- docs/specification/charter-v0.1.md | 29 ++++- docs/specification/core-v0.1.md | 19 ++- test-fixtures/spec/charter-v0.1/manifest.json | 14 +++ .../spec/charter-v0.1/manifest.schema.json | 54 +++++++- test-fixtures/spec/suite-v0.1/index.json | 27 ++++ .../spec/suite-v0.1/index.schema.json | 46 +++++++ test/charter-spec.test.ts | 63 +++++++++- test/specification-suite.test.ts | 116 +++++++++++++++++- 9 files changed, 373 insertions(+), 10 deletions(-) diff --git a/docs/specification/README.md b/docs/specification/README.md index 06f7b70af..417fce7ce 100644 --- a/docs/specification/README.md +++ b/docs/specification/README.md @@ -20,6 +20,13 @@ The derived release index is attached to the tag or release; it is not committed back onto the same commit. This records an immutable revision in the suite index without a self-referential hash that would invalidate the commit. +Publication metadata is not a conformance-claim binding. Every claimable +profile instead carries a non-null `revision` object in the suite registry and +its executable manifest. For Charter v0.1, that object binds the exact Charter +document bytes and the canonical manifest evidence payload with SHA-256 +digests. This makes working-draft claims reproducible while publication +metadata remains null. + **Document class:** Informative registry and navigation index **Foundation profile:** [Squad Charter Profile v0.1](charter-v0.1.md) @@ -61,7 +68,7 @@ deviations are summarized here and expanded in the |---|---|---|---|---|---|---|---| | `squad-core/v0.1` | `experimental-normative-draft` / normative profile | None | None | None | Normative rules are exercised by Charter; no standalone Core manifest | Canonical JSON and standalone Core cases incomplete | [Core](core-v0.1.md), then Charter | | `squad-interop/v0.1` | `requirements-draft` / requirements | Core | None | Reference evidence schema; no manifest | Schema and example records inform future profile design | No discovery, relational-invariant, or replay corpus | [Interoperability](interoperability-conventions-v0.1.md) | -| `squad-charter/v0.1` | `executable-normative-draft` / normative profile | Core | `squad-charter/v0.1/parse`, `squad-charter/v0.1/validate`, `squad-charter/v0.1/edit`, `squad-charter/v0.1/legacy-consume`, `squad-charter/v0.1/runtime` | Manifest and schema | Parser, validator, editor, compiler, and cases implemented | None registered | [Charter](charter-v0.1.md) | +| `squad-charter/v0.1` | `executable-normative-draft` / normative profile | Core | `squad-charter/v0.1/parse`, `squad-charter/v0.1/validate`, `squad-charter/v0.1/edit`, `squad-charter/v0.1/legacy-consume`, `squad-charter/v0.1/runtime` | Manifest, schema, and immutable revision binding | Parser, validator, editor, compiler, and cases implemented | None registered | [Charter](charter-v0.1.md) | | `squad-team-routing/v0.1` | `requirements-draft` / requirements | Core, Interop, Charter | None | None | Roster and routing exist in multiple shapes | Fallback and ambiguity differ | [Roster, then routing](team-routing-v0.1.md) | | `squad-configuration-casting/v0.1` | `requirements-draft` / requirements | Core, Interop, Team/Routing | None | None | Configuration, registry, and history implemented | Legacy shapes and runtime-only keys | [Configuration, casting, provenance](configuration-casting-v0.1.md) | | `squad-governance-review/v0.1` | `requirements-draft` / requirements | Core, Interop, Team/Routing | None | Reference schema and rejection fixture; no manifest | TypeScript checks exercise implementation expectations only | No portable relational-invariant or transition corpus | [Evidence shape, then promotion criteria](governance-review-v0.1.md) | @@ -79,7 +86,7 @@ The machine-readable registry is [`test-fixtures/spec/suite-v0.1/index.json`](../../test-fixtures/spec/suite-v0.1/index.json). The governance JSON is a reference fixture, not portable conformance evidence. Charter v0.1 is the only registered profile with a manifest, conformance -classes, and capability identifiers. +classes, capability identifiers, and immutable claim revisions. ## Textual dependency adjacency and DAG @@ -154,4 +161,6 @@ versions, implementation versions, and the observed portable-bundle `1.0` version are distinct. Draft documents may change before promotion. Only a `publication.status` of `published` denotes an immutable publication; published normative revisions are immutable except for dated errata, and behavior changes -require a new profile version. +require a new profile version. Claimable working drafts remain bound to exact +content digests through each profile's `revision` object; mutable branch names, +pull request numbers, and null publication fields are never claim revisions. diff --git a/docs/specification/charter-v0.1.md b/docs/specification/charter-v0.1.md index 12be9b028..d5e93baf1 100644 --- a/docs/specification/charter-v0.1.md +++ b/docs/specification/charter-v0.1.md @@ -56,6 +56,8 @@ by `.squad/agents/{id}/charter.md` provide no profile path context. RFC 8174, defines requirement-keyword interpretation. - [CommonMark 0.31.2](https://spec.commonmark.org/0.31.2/) defines the Markdown terms used by the deterministic subset below. +- [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785) defines the JSON + Canonicalization Scheme used for the evidence revision digest. ## 3. Association and unsupported versions @@ -297,8 +299,8 @@ The language-neutral field contract is [`manifest.schema.json`](../../test-fixtures/spec/charter-v0.1/manifest.schema.json). Every manifest contains: -- `$schema`, `schemaVersion`, `profile`, the complete `capabilities` list, and - uniquely identified `cases`; +- `$schema`, `schemaVersion`, `profile`, an immutable `revision` binding, the + complete `capabilities` list, and uniquely identified `cases`; - per-case `capabilities`, exactly one input source (`fixture` or `text`), nullable path/profile inputs, and the portable conformance API; - an always-present validation expectation with classification, capability @@ -316,6 +318,29 @@ Harnesses MUST validate the manifest against `manifest.schema.json` before executing cases. A manifest that fails schema validation is not a conformance suite and MUST NOT be partially executed. +### 12.1 Immutable claim revision + +A Charter conformance claim MUST copy the manifest's `revision.specification` +and `revision.evidence` objects without modification. Both revisions use +SHA-256 and lowercase hexadecimal digests prefixed by `sha256:`. + +The specification digest is computed over the exact UTF-8 bytes of +`docs/specification/charter-v0.1.md`. No line-ending normalization, Unicode +normalization, or whitespace transformation is applied. + +The evidence digest is computed over the manifest after removing only the +top-level `revision` member. The remaining JSON value is canonicalized using +the JSON Canonicalization Scheme in RFC 8785, identified in the manifest as +`rfc8785`. The SHA-256 digest is computed over the resulting UTF-8 bytes. + +The manifest schema permits only integers, strings, booleans, nulls, arrays, +and objects in the evidence projection. It does not permit non-integer numbers, +so the RFC 8785 number-serialization rules have one unambiguous integer input +domain here. Harnesses MUST recompute both digests before executing cases and +MUST reject a manifest when either digest disagrees. A branch name, pull +request number, publication revision, or other mutable reference is not a +substitute for either digest. + ## 13. Informative TypeScript implementation details The exported TypeScript names, module paths, and example below are informative. diff --git a/docs/specification/core-v0.1.md b/docs/specification/core-v0.1.md index 83d1d6e0e..fb7d5be91 100644 --- a/docs/specification/core-v0.1.md +++ b/docs/specification/core-v0.1.md @@ -55,8 +55,8 @@ appear in all capitals, as shown here. evidence surface. - **Capability:** one exact `/` identifier. - **Conformance claim:** a statement binding an implementation and version to - an exact profile revision, capability identifiers, applicable conformance - class, and evidence revision. + exact immutable specification and evidence revisions, capability identifiers, + and an applicable conformance class. - **Squad root:** the `.squad/` directory selected by an implementation. - **Artifact:** a file or directory beneath the Squad root described by a profile. @@ -122,6 +122,21 @@ consumer MAY expose raw bytes without interpreting them. Core version, profile version, implementation version, and mutable state revision are independent. None implies another. +### 6.1 Conformance revision binding + +Every conformance claim MUST identify both the exact specification revision and +the exact evidence revision against which conformance was established. Each +revision MUST use an immutable, machine-readable identifier whose derivation is +defined by the applicable profile. A content digest is an immutable revision +identifier when the profile defines the hashed bytes or canonical projection. + +Mutable branch names, pull request numbers, human-readable draft labels, and +nullable publication metadata MUST NOT be used as either revision identifier. +A working draft MAY be claimable when its profile supplies valid immutable +specification and evidence revisions; working-draft status does not relax this +requirement. Publication status, maturity, claimability, and claim revision are +independent. + ## 7. Common representation rules Text artifacts MUST be UTF-8. Producers SHOULD write LF line endings and MUST diff --git a/test-fixtures/spec/charter-v0.1/manifest.json b/test-fixtures/spec/charter-v0.1/manifest.json index 42ab97e0c..48689059b 100644 --- a/test-fixtures/spec/charter-v0.1/manifest.json +++ b/test-fixtures/spec/charter-v0.1/manifest.json @@ -2,6 +2,20 @@ "$schema": "./manifest.schema.json", "schemaVersion": 1, "profile": "squad-charter/v0.1", + "revision": { + "specification": { + "artifact": "docs/specification/charter-v0.1.md", + "algorithm": "sha256", + "digest": "sha256:bac80dbac473cac35824ffdf0d7e219a96945f7e82b00c24936e8e707bd5b9e6" + }, + "evidence": { + "artifact": "test-fixtures/spec/charter-v0.1/manifest.json", + "algorithm": "sha256", + "canonicalization": "rfc8785", + "scope": "manifest-excluding-revision", + "digest": "sha256:1f09c1954e056ea2072e3c79e286869790ae69751cba0ca4cfd58fd2fa0a92bb" + } + }, "capabilities": [ "squad-charter/v0.1/parse", "squad-charter/v0.1/validate", diff --git a/test-fixtures/spec/charter-v0.1/manifest.schema.json b/test-fixtures/spec/charter-v0.1/manifest.schema.json index 6a2b9b109..28609070f 100644 --- a/test-fixtures/spec/charter-v0.1/manifest.schema.json +++ b/test-fixtures/spec/charter-v0.1/manifest.schema.json @@ -3,7 +3,7 @@ "title": "Squad Charter Profile v0.1 conformance manifest", "type": "object", "additionalProperties": false, - "required": ["$schema", "schemaVersion", "profile", "capabilities", "cases"], + "required": ["$schema", "schemaVersion", "profile", "revision", "capabilities", "cases"], "properties": { "$schema": { "const": "./manifest.schema.json" @@ -14,6 +14,9 @@ "profile": { "const": "squad-charter/v0.1" }, + "revision": { + "$ref": "#/$defs/revision" + }, "capabilities": { "type": "array", "uniqueItems": true, @@ -30,6 +33,55 @@ } }, "$defs": { + "digest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "revision": { + "type": "object", + "additionalProperties": false, + "required": ["specification", "evidence"], + "properties": { + "specification": { + "type": "object", + "additionalProperties": false, + "required": ["artifact", "algorithm", "digest"], + "properties": { + "artifact": { + "const": "docs/specification/charter-v0.1.md" + }, + "algorithm": { + "const": "sha256" + }, + "digest": { + "$ref": "#/$defs/digest" + } + } + }, + "evidence": { + "type": "object", + "additionalProperties": false, + "required": ["artifact", "algorithm", "canonicalization", "scope", "digest"], + "properties": { + "artifact": { + "const": "test-fixtures/spec/charter-v0.1/manifest.json" + }, + "algorithm": { + "const": "sha256" + }, + "canonicalization": { + "const": "rfc8785" + }, + "scope": { + "const": "manifest-excluding-revision" + }, + "digest": { + "$ref": "#/$defs/digest" + } + } + } + } + }, "capability": { "enum": [ "squad-charter/v0.1/parse", diff --git a/test-fixtures/spec/suite-v0.1/index.json b/test-fixtures/spec/suite-v0.1/index.json index 20b5a1292..137199f3b 100644 --- a/test-fixtures/spec/suite-v0.1/index.json +++ b/test-fixtures/spec/suite-v0.1/index.json @@ -34,6 +34,7 @@ "maturity": "experimental-normative-draft", "documentClass": "normative-profile", "spec": "docs/specification/core-v0.1.md", + "revision": null, "dialect": "normative-draft", "dependencies": [], "conformanceClasses": [], @@ -52,6 +53,7 @@ "maturity": "requirements-draft", "documentClass": "requirements", "spec": "docs/specification/interoperability-conventions-v0.1.md", + "revision": null, "dialect": "requirements-only", "dependencies": ["squad-core/v0.1"], "conformanceClasses": [], @@ -70,6 +72,20 @@ "maturity": "executable-normative-draft", "documentClass": "normative-profile", "spec": "docs/specification/charter-v0.1.md", + "revision": { + "specification": { + "artifact": "docs/specification/charter-v0.1.md", + "algorithm": "sha256", + "digest": "sha256:bac80dbac473cac35824ffdf0d7e219a96945f7e82b00c24936e8e707bd5b9e6" + }, + "evidence": { + "artifact": "test-fixtures/spec/charter-v0.1/manifest.json", + "algorithm": "sha256", + "canonicalization": "rfc8785", + "scope": "manifest-excluding-revision", + "digest": "sha256:1f09c1954e056ea2072e3c79e286869790ae69751cba0ca4cfd58fd2fa0a92bb" + } + }, "dialect": "charter-v0-1", "dependencies": ["squad-core/v0.1"], "conformanceClasses": ["canonical", "compatible", "legacy", "runtime"], @@ -94,6 +110,7 @@ "maturity": "requirements-draft", "documentClass": "requirements", "spec": "docs/specification/team-routing-v0.1.md", + "revision": null, "dialect": "requirements-only", "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-charter/v0.1"], "conformanceClasses": [], @@ -112,6 +129,7 @@ "maturity": "requirements-draft", "documentClass": "requirements", "spec": "docs/specification/configuration-casting-v0.1.md", + "revision": null, "dialect": "requirements-only", "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-team-routing/v0.1"], "conformanceClasses": [], @@ -130,6 +148,7 @@ "maturity": "requirements-draft", "documentClass": "requirements", "spec": "docs/specification/governance-review-v0.1.md", + "revision": null, "dialect": "requirements-only", "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-team-routing/v0.1"], "conformanceClasses": [], @@ -150,6 +169,7 @@ "maturity": "requirements-draft", "documentClass": "requirements", "spec": "docs/specification/execution-preferences-v0.1.md", + "revision": null, "dialect": "requirements-only", "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-charter/v0.1", "squad-configuration-casting/v0.1"], "conformanceClasses": [], @@ -168,6 +188,7 @@ "maturity": "requirements-draft", "documentClass": "requirements", "spec": "docs/specification/work-lifecycle-v0.1.md", + "revision": null, "dialect": "requirements-only", "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-team-routing/v0.1", "squad-governance-review/v0.1"], "conformanceClasses": [], @@ -186,6 +207,7 @@ "maturity": "requirements-draft", "documentClass": "requirements", "spec": "docs/specification/github-work-lifecycle-binding-v0.1.md", + "revision": null, "dialect": "requirements-only", "dependencies": ["squad-interop/v0.1", "squad-work-lifecycle/v0.1", "squad-governance-review/v0.1"], "conformanceClasses": [], @@ -204,6 +226,7 @@ "maturity": "requirements-draft", "documentClass": "requirements", "spec": "docs/specification/planning-activation-v0.1.md", + "revision": null, "dialect": "requirements-only", "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-team-routing/v0.1", "squad-work-lifecycle/v0.1"], "conformanceClasses": [], @@ -222,6 +245,7 @@ "maturity": "requirements-draft", "documentClass": "requirements", "spec": "docs/specification/ceremony-v0.1.md", + "revision": null, "dialect": "requirements-only", "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-team-routing/v0.1"], "conformanceClasses": [], @@ -240,6 +264,7 @@ "maturity": "requirements-draft", "documentClass": "requirements", "spec": "docs/specification/coordination-handoff-v0.1.md", + "revision": null, "dialect": "requirements-only", "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-team-routing/v0.1"], "conformanceClasses": [], @@ -258,6 +283,7 @@ "maturity": "requirements-draft", "documentClass": "requirements", "spec": "docs/specification/automation-v0.1.md", + "revision": null, "dialect": "requirements-only", "dependencies": ["squad-core/v0.1", "squad-interop/v0.1", "squad-coordination-handoff/v0.1"], "conformanceClasses": [], @@ -276,6 +302,7 @@ "maturity": "informative-draft", "documentClass": "informative", "spec": "docs/specification/state-memory-requirements-v0.1.md", + "revision": null, "dialect": "requirements-only", "dependencies": ["squad-core/v0.1", "squad-interop/v0.1"], "conformanceClasses": [], diff --git a/test-fixtures/spec/suite-v0.1/index.schema.json b/test-fixtures/spec/suite-v0.1/index.schema.json index 5fafc562e..05d21543e 100644 --- a/test-fixtures/spec/suite-v0.1/index.schema.json +++ b/test-fixtures/spec/suite-v0.1/index.schema.json @@ -70,6 +70,39 @@ "type": "string", "pattern": "^squad-[a-z0-9-]+/v(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)/[a-z0-9-]+$" }, + "digest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "revision": { + "type": "object", + "additionalProperties": false, + "required": ["specification", "evidence"], + "properties": { + "specification": { + "type": "object", + "additionalProperties": false, + "required": ["artifact", "algorithm", "digest"], + "properties": { + "artifact": { "$ref": "#/$defs/repoPath" }, + "algorithm": { "const": "sha256" }, + "digest": { "$ref": "#/$defs/digest" } + } + }, + "evidence": { + "type": "object", + "additionalProperties": false, + "required": ["artifact", "algorithm", "canonicalization", "scope", "digest"], + "properties": { + "artifact": { "$ref": "#/$defs/repoPath" }, + "algorithm": { "const": "sha256" }, + "canonicalization": { "const": "rfc8785" }, + "scope": { "const": "manifest-excluding-revision" }, + "digest": { "$ref": "#/$defs/digest" } + } + } + } + }, "dialect": { "type": "object", "additionalProperties": false, @@ -87,6 +120,7 @@ "maturity", "documentClass", "spec", + "revision", "dialect", "dependencies", "conformanceClasses", @@ -120,6 +154,12 @@ ] }, "spec": { "$ref": "#/$defs/repoPath" }, + "revision": { + "anyOf": [ + { "$ref": "#/$defs/revision" }, + { "type": "null" } + ] + }, "dialect": { "type": "string", "minLength": 1 }, "dependencies": { "type": "array", @@ -196,9 +236,15 @@ }, "then": { "properties": { + "revision": { "type": "null" }, "capabilities": { "type": "array", "maxItems": 0 }, "conformanceClasses": { "type": "array", "maxItems": 0 } } + }, + "else": { + "properties": { + "revision": { "$ref": "#/$defs/revision" } + } } }, { diff --git a/test/charter-spec.test.ts b/test/charter-spec.test.ts index 7dad53c64..cc51240b6 100644 --- a/test/charter-spec.test.ts +++ b/test/charter-spec.test.ts @@ -105,6 +105,20 @@ interface FixtureManifest { $schema: './manifest.schema.json'; schemaVersion: 1; profile: string; + revision: { + specification: { + artifact: string; + algorithm: 'sha256'; + digest: string; + }; + evidence: { + artifact: string; + algorithm: 'sha256'; + canonicalization: 'rfc8785'; + scope: 'manifest-excluding-revision'; + digest: string; + }; + }; capabilities: CharterCapability[]; cases: ManifestCase[]; } @@ -266,6 +280,7 @@ describe('Squad Charter Profile v0.1 portable conformance manifest', () => { } const { schemaVersion: _schemaVersion, ...missingRequired } = manifest; + const { revision: _revision, ...missingRevision } = manifest; const invalidManifests: Array<{ name: string; document: unknown; @@ -281,6 +296,40 @@ describe('Squad Charter Profile v0.1 portable conformance manifest', () => { document: missingRequired, keyword: 'required', }, + { + name: 'missing immutable revision binding', + document: missingRevision, + keyword: 'required', + }, + { + name: 'mutable specification revision', + document: { + ...manifest, + revision: { + ...manifest.revision, + specification: { + ...manifest.revision.specification, + algorithm: 'git-branch', + digest: 'dev', + }, + }, + }, + keyword: 'const', + }, + { + name: 'invalid evidence digest', + document: { + ...manifest, + revision: { + ...manifest.revision, + evidence: { + ...manifest.revision.evidence, + digest: 'sha256:not-a-digest', + }, + }, + }, + keyword: 'pattern', + }, { name: 'invalid validation result type', document: { @@ -377,12 +426,24 @@ describe('Squad Charter Profile v0.1 portable conformance manifest', () => { } }); - it('declares the published profile metadata and every operational capability', async () => { + it('declares the profile metadata, immutable revisions, and every operational capability', async () => { const manifest = await loadManifest(); const schema = await loadManifestSchema(); expect(manifest.$schema).toBe('./manifest.schema.json'); expect(manifest.schemaVersion).toBe(1); expect(manifest.profile).toBe(CHARTER_PROFILE); + expect(manifest.revision).toMatchObject({ + specification: { + artifact: 'docs/specification/charter-v0.1.md', + algorithm: 'sha256', + }, + evidence: { + artifact: 'test-fixtures/spec/charter-v0.1/manifest.json', + algorithm: 'sha256', + canonicalization: 'rfc8785', + scope: 'manifest-excluding-revision', + }, + }); expect(manifest.capabilities).toEqual(Object.values(CHARTER_CAPABILITIES)); expect(schema.$defs.capability.enum).toEqual(manifest.capabilities); expect(schema.$defs.diagnostic.properties.code.enum) diff --git a/test/specification-suite.test.ts b/test/specification-suite.test.ts index 43ccd3540..09f0d4c42 100644 --- a/test/specification-suite.test.ts +++ b/test/specification-suite.test.ts @@ -1,3 +1,4 @@ +import { createHash } from 'node:crypto'; import { access, readFile, readdir } from 'node:fs/promises'; import * as path from 'node:path'; import Ajv2020, { type AnySchema, type ValidateFunction } from 'ajv/dist/2020.js'; @@ -40,6 +41,27 @@ function clone(value: T): T { return structuredClone(value); } +function canonicalJson(value: unknown): string { + if (value === null || typeof value !== 'object') { + if (typeof value === 'number' && !Number.isInteger(value)) { + throw new TypeError('Charter v0.1 evidence permits integers only'); + } + return JSON.stringify(value); + } + if (Array.isArray(value)) { + return `[${value.map(item => canonicalJson(item)).join(',')}]`; + } + const record = value as Record; + return `{${Object.keys(record) + .sort() + .map(key => `${JSON.stringify(key)}:${canonicalJson(record[key])}`) + .join(',')}}`; +} + +function sha256(value: string | Buffer): string { + return `sha256:${createHash('sha256').update(value).digest('hex')}`; +} + async function markdownFiles(root: string): Promise { const entries = await readdir(root, { withFileTypes: true }); const nested = await Promise.all(entries.map(async entry => { @@ -397,7 +419,22 @@ describe('Squad specification suite v0.1', () => { profiles: Array<{ id: string; maturity: string; + documentClass: string; spec: string; + revision: { + specification: { + artifact: string; + algorithm: string; + digest: string; + }; + evidence: { + artifact: string; + algorithm: string; + canonicalization: string; + scope: string; + digest: string; + }; + } | null; dialect: string; dependencies: string[]; capabilities: string[]; @@ -419,12 +456,15 @@ describe('Squad specification suite v0.1', () => { expect(index.profiles.every(profile => !profile.maturity.startsWith('published'))).toBe(true); expect(index.profiles.find(profile => profile.id === 'squad-core/v0.1')).toMatchObject({ maturity: 'experimental-normative-draft', + revision: null, manifest: null, capabilities: [], conformanceClasses: [], }); - expect(index.profiles.find(profile => profile.id === 'squad-charter/v0.1')).toMatchObject({ + const charterProfile = index.profiles.find(profile => profile.id === 'squad-charter/v0.1'); + expect(charterProfile).toMatchObject({ maturity: 'executable-normative-draft', + documentClass: 'normative-profile', }); const claimableProfiles = index.profiles.filter(profile => profile.manifest !== null || @@ -454,6 +494,7 @@ describe('Squad specification suite v0.1', () => { expect(profile.entryPoint, profile.id).not.toHaveLength(0); if (profile.manifest === null) { + expect(profile.revision, profile.id).toBeNull(); expect(profile.manifestSchema, profile.id).toBeNull(); expect(profile.capabilities, profile.id).toEqual([]); expect(profile.conformanceClasses, profile.id).toEqual([]); @@ -462,6 +503,7 @@ describe('Squad specification suite v0.1', () => { expect(profile.manifestSchema, profile.id).not.toBeNull(); const manifest = JSON.parse(await readFile(path.join(ROOT, profile.manifest), 'utf8')) as { profile: string; + revision: NonNullable; capabilities: string[]; cases: Array<{ id: string }>; }; @@ -472,7 +514,15 @@ describe('Squad specification suite v0.1', () => { expect(validate(manifest), `${profile.id}: ${JSON.stringify(validate.errors, null, 2)}`).toBe(true); expect(manifest.profile).toBe(profile.id); + expect(profile.revision).toEqual(manifest.revision); expect(profile.capabilities).toEqual(manifest.capabilities); + expect(manifest.revision.specification.artifact).toBe(profile.spec); + expect(manifest.revision.evidence.artifact).toBe(profile.manifest); + expect(manifest.revision.specification.digest).toBe( + sha256(await readFile(path.join(ROOT, profile.spec))), + ); + const { revision: _manifestRevision, ...evidencePayload } = manifest; + expect(manifest.revision.evidence.digest).toBe(sha256(canonicalJson(evidencePayload))); for (const testCase of manifest.cases) { const qualifiedId = `${profile.id}/${testCase.id}`; expect(caseIds.has(qualifiedId), qualifiedId).toBe(false); @@ -491,6 +541,70 @@ describe('Squad specification suite v0.1', () => { } }); + it('keeps the human and machine Charter registries in semantic agreement', async () => { + const index = await readJson('index.json') as { + profiles: Array<{ + id: string; + maturity: string; + documentClass: string; + dependencies: string[]; + conformanceClasses: string[]; + capabilities: string[]; + manifest: string | null; + manifestSchema: string | null; + revision: unknown; + }>; + }; + const charter = index.profiles.find(profile => profile.id === 'squad-charter/v0.1'); + expect(charter).toBeDefined(); + if (charter === undefined) throw new Error('Charter profile is missing'); + + const readme = await readFile(path.join(SPEC_ROOT, 'README.md'), 'utf8'); + const row = readme.split('\n').find(line => line.startsWith('| `squad-charter/v0.1` |')); + expect(row).toBeDefined(); + if (row === undefined) throw new Error('Charter registry row is missing'); + + expect(row).toContain(`\`${charter.maturity}\` / ${charter.documentClass.replace('-', ' ')}`); + expect(row).toContain('| Core |'); + expect(charter.dependencies).toEqual(['squad-core/v0.1']); + expect(charter.conformanceClasses).toEqual(['canonical', 'compatible', 'legacy', 'runtime']); + for (const capability of charter.capabilities) expect(row).toContain(`\`${capability}\``); + expect(row).toContain('Manifest, schema, and immutable revision binding'); + expect(charter.manifest).toBe('test-fixtures/spec/charter-v0.1/manifest.json'); + expect(charter.manifestSchema).toBe('test-fixtures/spec/charter-v0.1/manifest.schema.json'); + expect(charter.revision).not.toBeNull(); + }); + + it('rejects missing, mutable, and invalid claim revision bindings', async () => { + const schema = await readJson('index.schema.json') as AnySchema; + const validate = createAjv().compile(schema); + const index = await readJson('index.json') as { + profiles: Array>; + }; + const charterIndex = index.profiles.findIndex( + profile => profile['id'] === 'squad-charter/v0.1', + ); + expect(charterIndex).toBeGreaterThanOrEqual(0); + + const mutations: Array<(profile: Record) => void> = [ + profile => { delete profile['revision']; }, + profile => { + const revision = profile['revision'] as Record>; + revision['specification']['digest'] = 'branch:dev'; + }, + profile => { + const revision = profile['revision'] as Record>; + revision['evidence']['canonicalization'] = 'JSON.stringify'; + }, + ]; + + for (const mutate of mutations) { + const invalid = clone(index); + mutate(invalid.profiles[charterIndex]); + expect(validate(invalid), JSON.stringify(validate.errors, null, 2)).toBe(false); + } + }); + it('detects a cyclic suite dependency graph', async () => { const index = await readJson('index.json') as { profiles: Array<{ id: string; dependencies: string[] }>; From 24ebb9074d191c4a25c6217293ef9588d015544c Mon Sep 17 00:00:00 2001 From: brady gaster Date: Fri, 2 Oct 2026 12:42:36 -0700 Subject: [PATCH 10/11] docs: clarify specification scope and execution boundaries Part of #2069 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/specification/README.md | 5 +- docs/specification/artifact-inventory.md | 29 ++++++--- docs/specification/ceremony-v0.1.md | 65 +++++++++++++++++++ docs/specification/charter-v0.1.md | 25 +++++++ test-fixtures/spec/charter-v0.1/manifest.json | 2 +- test-fixtures/spec/suite-v0.1/index.json | 2 +- 6 files changed, 117 insertions(+), 11 deletions(-) diff --git a/docs/specification/README.md b/docs/specification/README.md index 417fce7ce..585d227e9 100644 --- a/docs/specification/README.md +++ b/docs/specification/README.md @@ -31,7 +31,10 @@ metadata remains null. **Foundation profile:** [Squad Charter Profile v0.1](charter-v0.1.md) -This directory maps all major stable or observable Squad surfaces. Publication +This directory maps a working inventory of stable or observable Squad surfaces, +including composition and initialization capabilities without profiles in +the [artifact inventory](artifact-inventory.md). It does not yet specify the +complete model for how independent Squads compose and interact. Publication status is independent from maturity: this suite is an unpublished working draft. Charter is the only profile with executable conformance evidence. diff --git a/docs/specification/artifact-inventory.md b/docs/specification/artifact-inventory.md index ce4e215c3..3f5783f45 100644 --- a/docs/specification/artifact-inventory.md +++ b/docs/specification/artifact-inventory.md @@ -7,15 +7,22 @@ **Purpose:** Repository-derived traceability, profile planning, and interoperability maturity evidence for issue #2069. -Paths in this table are Squad-root-relative. Repository-local installations -render them beneath `.squad/`. +Artifact paths such as `agents/{id}/charter.md` are Squad-root-relative; +repository-local installations render them beneath `.squad/`. Implementation +links instead point to repository source, templates, and tests. -| Surface | Repository evidence | Disposition | Specification | Maturity or promotion rationale | +This is a working inventory, not a complete specification of Squad composition. +The current suite focuses on artifacts and behavior within one Squad, with +separate observations of existing composition surfaces below. SubSquads, +upstream inheritance, peer discovery, and cross-Squad delegation have different +semantics; none acquires a portable contract from being listed here. + +| Capability | Implementation / artifacts | Spec status | Spec coverage | What's missing | |---|---|---|---|---| | Discovery, negotiation, authority identity, shared evidence, secrets | Core/profile metadata, registry IDs, events, audit records | Requirements draft with a reference schema; no claim | Interoperability Conventions v0.1 | Discovery, relational invariants, and negotiation need portable manifests and cases | | Core rules | profile identifiers, paths, diagnostics, trust boundary | Experimental normative working draft; no standalone claim | Core v0.1 | Charter depends on and exercises Core rules, but Core has no standalone manifest | -| Agent charter | `agents/{id}/charter.md`, compiler, validator, fixtures | Executable normative working draft | Charter v0.1 | Stable parser and round-trip evidence; publication remains `working-draft` | -| Team roster | `team.md`, roster templates, presets | Requirements draft; no claim | Team and Routing v0.1 | Promote independently with roster parsing, mutation, join, and diagnostic cases | +| Agent charter | `agents/{id}/charter.md`; [compiler](../../packages/squad-sdk/src/agents/charter-compiler.ts), [validator](../../packages/squad-sdk/src/agents/charter-validator.ts), [fixture manifest](../../test-fixtures/spec/charter-v0.1/manifest.json) | Executable normative working draft | [Charter v0.1](charter-v0.1.md) | Stable parser and round-trip evidence; publication remains `working-draft` | +| Team roster | `team.md`; [initialization and roster generation](../../packages/squad-sdk/src/config/init.ts) | Requirements draft; no claim | Team and Routing v0.1 | Promote independently with roster parsing, mutation, join, and diagnostic cases | | Routing ownership | `routing.md`, routing parser, coordinator tests | Requirements draft; no claim | Team and Routing v0.1 | Promote independently with priority, fallback, ambiguity, stale-write, and resolution cases | | Runtime configuration | `config.json`, config schema and loaders | Requirements draft; no claim | Configuration and Casting v0.1 | Promote independently with precedence and source-evidence cases | | Casting identity | `casting/registry.json`, `policy.json` | Requirements draft; no claim | Configuration and Casting v0.1 | Promote independently with identity, recast, join, and migration cases | @@ -23,14 +30,20 @@ render them beneath `.squad/`. | Governance and review | `governance.md`, reviewer protocol, PR requirements | Requirements draft with reference checks; no claim | Governance and Review v0.1 | Add language-neutral relational invariants and a complete transition manifest | | Execution preferences | charters, config, model selector, lifecycle manager | Requirements draft; no claim | Execution Preferences v0.1 | Add provider-neutral resolution cases and loss reporting | | PRD intake and planning | `prd-intake.md`, planning ontology, gh-aw workflows | Requirements draft; no claim | Planning and Activation v0.1 | Add portable planning-record and activation manifest | -| Ceremonies | `ceremonies.md`, ceremony reference, retro workflow | Requirements draft; no claim | Ceremony v0.1 | Normalize event vocabulary and prove cooldown with injected clocks | +| Ceremonies | [declaration template](../../.squad-templates/ceremonies.md), [ceremony reference](../../.squad-templates/ceremony-reference.md), [SDK builder](../../packages/squad-sdk/src/builders/index.ts), [retro workflow](../../workflows/squad-retro.md) | Requirements draft; no claim | [Ceremony v0.1](ceremony-v0.1.md) | Map definition/agenda/hooks separately from shared scheduling and execution; prove cooldown with injected clocks | | Work lifecycle | issue lifecycle template, platform interfaces, worktree tests | Requirements draft; no claim | Work Lifecycle v0.1 | Run complete transition traces against two providers | | GitHub lifecycle | GitHub adapter, gh-aw workflows, issue/PR templates | Requirements binding draft; no claim | GitHub Work Lifecycle Binding v0.1 | Add deterministic sanitized provider transcripts | | Handoffs and sessions | spawn reference, lifecycle manager, event buses | Requirements draft; no claim | Coordination and Handoff v0.1 | Add portable envelope, ordering, expiry, and recovery cases | | State backends | state backend interfaces, worktree/orphan/two-layer implementations | Informative requirements now | State and Memory Requirements v0.1 | Authority and recovery vary by backend; promotion requires backend-neutral CAS fixtures | | Memory governance | memory classes, providers, audit records | Informative requirements now | State and Memory Requirements v0.1 | Classification is promising but provider naming and durable authority are not stable | -| Automation | schedule manifest, scheduler, local and GitHub providers | Requirements draft; no claim | Automation v0.1 | Add portable trigger, clock, retry, drift, and result cases | -| Portable export/import | `squad export`, `squad import`, CLI round-trip tests | Informative observed surface; no claim | `squad-portable-bundle/v1.0` coverage below | Tested implementation exists without a published schema or conformance manifest | +| Automation | `schedule.json`; [scheduler and provider interfaces](../../packages/squad-sdk/src/runtime/scheduler.ts), [schedule CLI](../../packages/squad-cli/src/cli/commands/schedule.ts) | Requirements draft; no claim | [Automation v0.1](automation-v0.1.md) | Add portable trigger, clock, retry, drift, and result cases | +| Portable export/import | [`squad export`](../../packages/squad-cli/src/cli/commands/export.ts), [`squad import`](../../packages/squad-cli/src/cli/commands/import.ts) | Informative observed surface; no claim | `squad-portable-bundle/v1.0` coverage below | Tested implementation exists without a published schema or conformance manifest | +| Preset/template instantiation | [preset SDK](../../packages/squad-sdk/src/presets/index.ts), [preset CLI](../../packages/squad-cli/src/cli/commands/preset.ts), [initialization](../../packages/squad-sdk/src/config/init.ts) | Informative observed surface; no claim | No dedicated profile | Presets apply reusable agent charters; they are not full export/import snapshots. Definition versioning, initialization defaults, and conflict behavior need separate coverage | +| SubSquads / workstreams | [partition types](../../packages/squad-sdk/src/streams/types.ts), [resolver](../../packages/squad-sdk/src/streams/resolver.ts), [CLI](../../packages/squad-cli/src/cli/commands/streams.ts) | Informative observed surface; no claim | No dedicated profile | Scoped team partitions are distinct from independent peer Squads. Config path aliases, activation precedence, issue/folder scope, and isolation need portable cases | +| Upstream inheritance | [upstream types](../../packages/squad-sdk/src/upstream/types.ts), [resolver](../../packages/squad-sdk/src/upstream/resolver.ts), [CLI](../../packages/squad-cli/src/cli/commands/upstream.ts) | Informative observed surface; no claim | No dedicated profile | Context inheritance is distinct from discovery-only registration. Source revisions, precedence, trust, and synchronization need separate contracts | +| Peer Squad registry and discovery | `squad-registry.json`, `manifest.json`; [SDK](../../packages/squad-sdk/src/runtime/cross-squad.ts), [CLI](../../packages/squad-cli/src/cli/commands/cross-squad.ts) | Informative observed surface; no claim | No dedicated profile; not the specification discovery model | Registry entries identify peers without inheriting context. Manifest identity, stale discovery, and capability negotiation need portable cases | +| Cross-Squad communication and delegation | [discovery/delegation SDK](../../packages/squad-sdk/src/runtime/cross-squad.ts), [`squad delegate`](../../packages/squad-cli/src/cli/commands/cross-squad.ts) | Informative observed surface; no claim | Coordination and provider drafts are related, not a cross-Squad contract | GitHub issue-based work requests exist; delivery, acceptance, completion correlation, and delegation authority are not specified here | +| Creation/spawning of another Squad | [initialization API](../../packages/squad-sdk/src/config/init.ts), [preset application](../../packages/squad-sdk/src/presets/index.ts) | Scope gap; no claim | No dedicated profile | Local initialization is not evidence of autonomous cross-Squad spawning. Ownership, resource provisioning, isolation, and lifecycle require independent design and evidence | | Skills | `skills/{name}/SKILL.md`, loader and discovery | Deferred | Future Skills Profile | Promote after metadata precedence, imports, script trust, and compatibility fixtures stabilize | | Coordinator prompts and UI wording | templates, generated agent prompt, shell | Informative guidance | Implementation documentation | Prompts and wording are implementation inputs, not interoperability contracts | | Telemetry and cost accounting | OpenTelemetry and cost modules | Deferred | Future Observability Binding | Promote after common event identity and privacy/redaction contracts stabilize | diff --git a/docs/specification/ceremony-v0.1.md b/docs/specification/ceremony-v0.1.md index 02da77714..4e52a0fb1 100644 --- a/docs/specification/ceremony-v0.1.md +++ b/docs/specification/ceremony-v0.1.md @@ -29,6 +29,12 @@ participants, decisions, actions, and completion records. It does not standardize meeting culture, agenda prose, facilitation prompts, timekeeping style, consensus, or synchronous human attendance. +This requirements draft is not a complete specification of today's ceremony +extensibility surface. It separates three concerns: a definition describes what +happens; shared scheduling describes when/how a trigger is delivered; an +execution record describes one run. The proposed execution lifecycle below is +not evidence that all of it is implemented. + ## 2. Declaration model A ceremony declares ID, enabled state, trigger mode, timing, condition @@ -44,6 +50,44 @@ Participant selectors are `accountable-owner`, `all-involved`, `all-relevant`, or explicit member IDs. Resolution must produce the exact participant set used. +### 2.1 Definition and current implementation mapping + +A definition also preserves its agenda/process, participant roles, inputs, +expected outputs, conditions, and extension hooks without standardizing their +prose or silently executing hooks. The +[SDK `defineCeremony()` builder](../../packages/squad-sdk/src/builders/index.ts) +accepts name, trigger, schedule, participants, agenda, and hooks. The +[configuration shape](../../packages/squad-sdk/src/config/schema.ts) and +[Markdown template](../../.squad-templates/ceremonies.md) are distinct existing +representations; the template includes agenda, timing, facilitator, conditions, +and an enforcement skill. The +[ceremony reference](../../.squad-templates/ceremony-reference.md) supplies +implementation guidance. + +These are observations, not a claim of lossless conversion between those +shapes. Hook invocation, process ordering, required input/output schemas, +plugin/template merge behavior, and a complete mapping of existing fields +remain promotion work. + +### 2.2 Shared trigger and scheduling boundary + +The [Automation requirements draft](automation-v0.1.md#2-manifest-model) owns +the proposed common `schedule.json` model, including cron, interval, event, and +startup triggers. Its [time semantics](automation-v0.1.md#4-trigger-and-time-semantics) +are shared scheduling concerns, not ceremony-specific definitions. The +[scheduler](../../packages/squad-sdk/src/runtime/scheduler.ts) and +[schedule CLI](../../packages/squad-cli/src/cli/commands/schedule.ts) implement +the current common scheduling surface. + +A future binding should reference a ceremony definition from a scheduled task +and supply trigger evidence to its executor, rather than define another cron +or retry engine here. The ceremony `manual`/`schedule`/`event:` vocabulary +above is a proposed declaration vocabulary, not a second `schedule.json` +schema. Mapping builder schedule text and Markdown timing/conditions to common +schedules, including non-cron triggers, is not yet specified or proven. +Eligibility evaluation and delivery belong to scheduling; ceremony-specific +cooldown, facilitation, and outcomes belong to execution. + ## 3. Execution record Every run records ceremony ID, run ID, trigger evidence, input revisions, @@ -67,6 +111,21 @@ Candidate operations are `ceremony.consume`, `ceremony.evaluate`, run; participants provide inputs; the recorder persists outcomes; the coordinator enforces claim and cooldown. +These names are non-contract placeholders for future profile work, not current +SDK entry points, claimable capabilities, or a normative interoperable API. +The intended responsibilities are: + +| Placeholder | Intended caller and input | Intended result | +|---|---|---| +| `ceremony.consume` | A loader reads a declaration and its format/version context | Parsed definition or diagnostics | +| `ceremony.evaluate` | A coordinator supplies a definition, trigger evidence, current inputs, and run history | Eligibility/suppression decision with reasons | +| `ceremony.facilitate` | An authorized executor supplies a claimed run, resolved participants, and definition | Proposed decisions/actions or an explicit failure | +| `ceremony.record` | A recorder receives execution outcomes and expected state revision | Persisted run/outcome references or a conflict | + +Exact types, invocation order, authorization, failure taxonomy, and atomicity +are deliberately unresolved. These descriptions do not authorize invocation +or establish an operation contract. + ## 6. Canonical and compatible behavior Canonical declarations use deterministic trigger references and participant @@ -92,3 +151,9 @@ Publish a manifest covering normalized manual, named-event, schedule, and legacy-`auto` inputs; selector resolution; injected-clock cooldown suppression; advisory and blocking failures; duplicate claims; stale inputs; and decision/action output validation. + +Before promotion, map the SDK builder, configuration, Markdown templates, and +plugin hooks to a complete definition model; bind shared Automation scheduling +without duplicating it; and replace or remove the placeholders in §5 with +versioned input/output, caller, authorization, and failure contracts. Definition +round trips must preserve agenda and hook metadata without executing it. diff --git a/docs/specification/charter-v0.1.md b/docs/specification/charter-v0.1.md index d5e93baf1..391788d01 100644 --- a/docs/specification/charter-v0.1.md +++ b/docs/specification/charter-v0.1.md @@ -343,6 +343,31 @@ substitute for either digest. ## 13. Informative TypeScript implementation details +### 13.1 Portable charter and execution binding boundary + +The smaller portable shape is intentional: it identifies the agent and its +guidance/preferences, not every product configuration field. As specified in +[§3](#3-association-and-unsupported-versions), a copied charter still needs its +profile selection supplied by an API or accompanying manifest; it is not +self-describing. Adding an embedded version marker is a future profile-design +question, not a v0.1 requirement. + +An authenticated execution principal (for example, a managed or service +identity) belongs to trusted runtime configuration and provider binding, not +the charter's display or path identity. Tool requests likewise belong to +runtime/configuration integration; the runtime independently determines tool +availability and grants under [§11](#11-runtime-validation-and-security). +For example, the [current agent configuration](../../packages/squad-sdk/src/config/schema.ts) +has tool declarations; these declarations are not authorization. + +The suite does not yet define a portable principal-binding or tool-declaration +schema. [Interop authority requirements](interoperability-conventions-v0.1.md#3-stable-identity-and-authority) +are related design work, not an implemented execution binding. A namespaced +charter extension can preserve implementation metadata but cannot establish +authenticated identity or grant access. + +### 13.2 Reference API example + The exported TypeScript names, module paths, and example below are informative. They do not constrain implementations in other languages. diff --git a/test-fixtures/spec/charter-v0.1/manifest.json b/test-fixtures/spec/charter-v0.1/manifest.json index 48689059b..60bbf2356 100644 --- a/test-fixtures/spec/charter-v0.1/manifest.json +++ b/test-fixtures/spec/charter-v0.1/manifest.json @@ -6,7 +6,7 @@ "specification": { "artifact": "docs/specification/charter-v0.1.md", "algorithm": "sha256", - "digest": "sha256:bac80dbac473cac35824ffdf0d7e219a96945f7e82b00c24936e8e707bd5b9e6" + "digest": "sha256:2274102c633926cb7dd0c5fd890aec5822d78026f13d71d91d7dd336b6a274b3" }, "evidence": { "artifact": "test-fixtures/spec/charter-v0.1/manifest.json", diff --git a/test-fixtures/spec/suite-v0.1/index.json b/test-fixtures/spec/suite-v0.1/index.json index 137199f3b..c25fa8bbf 100644 --- a/test-fixtures/spec/suite-v0.1/index.json +++ b/test-fixtures/spec/suite-v0.1/index.json @@ -76,7 +76,7 @@ "specification": { "artifact": "docs/specification/charter-v0.1.md", "algorithm": "sha256", - "digest": "sha256:bac80dbac473cac35824ffdf0d7e219a96945f7e82b00c24936e8e707bd5b9e6" + "digest": "sha256:2274102c633926cb7dd0c5fd890aec5822d78026f13d71d91d7dd336b6a274b3" }, "evidence": { "artifact": "test-fixtures/spec/charter-v0.1/manifest.json", From ccc5143733bc01098029080445245ffaba5324df Mon Sep 17 00:00:00 2001 From: brady gaster Date: Fri, 2 Oct 2026 12:55:37 -0700 Subject: [PATCH 11/11] fix: bind Charter digest to portable checkout bytes Part of #2069 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .gitattributes | 5 ++++- test-fixtures/spec/charter-v0.1/manifest.json | 2 +- test-fixtures/spec/suite-v0.1/index.json | 2 +- test/specification-suite.test.ts | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.gitattributes b/.gitattributes index 44ce9e26b..862312bd9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -56,4 +56,7 @@ Dockerfile.* text eol=lf # Squad: union merge for append-only team state files .squad/fact-checker/audit-trail.md merge=union -.github/workflows/*.lock.yml linguist-generated=true \ No newline at end of file +.github/workflows/*.lock.yml linguist-generated=true + +# Charter conformance binds exact document bytes across platform checkouts. +docs/specification/charter-v0.1.md text eol=lf \ No newline at end of file diff --git a/test-fixtures/spec/charter-v0.1/manifest.json b/test-fixtures/spec/charter-v0.1/manifest.json index 60bbf2356..6ab4a447c 100644 --- a/test-fixtures/spec/charter-v0.1/manifest.json +++ b/test-fixtures/spec/charter-v0.1/manifest.json @@ -6,7 +6,7 @@ "specification": { "artifact": "docs/specification/charter-v0.1.md", "algorithm": "sha256", - "digest": "sha256:2274102c633926cb7dd0c5fd890aec5822d78026f13d71d91d7dd336b6a274b3" + "digest": "sha256:9bdc6440b83fbd087d7ac01129146dda067d86501b7d3ecf2e81134d5b7ce7c2" }, "evidence": { "artifact": "test-fixtures/spec/charter-v0.1/manifest.json", diff --git a/test-fixtures/spec/suite-v0.1/index.json b/test-fixtures/spec/suite-v0.1/index.json index c25fa8bbf..1420c9114 100644 --- a/test-fixtures/spec/suite-v0.1/index.json +++ b/test-fixtures/spec/suite-v0.1/index.json @@ -76,7 +76,7 @@ "specification": { "artifact": "docs/specification/charter-v0.1.md", "algorithm": "sha256", - "digest": "sha256:2274102c633926cb7dd0c5fd890aec5822d78026f13d71d91d7dd336b6a274b3" + "digest": "sha256:9bdc6440b83fbd087d7ac01129146dda067d86501b7d3ecf2e81134d5b7ce7c2" }, "evidence": { "artifact": "test-fixtures/spec/charter-v0.1/manifest.json", diff --git a/test/specification-suite.test.ts b/test/specification-suite.test.ts index 09f0d4c42..867a06d6f 100644 --- a/test/specification-suite.test.ts +++ b/test/specification-suite.test.ts @@ -518,7 +518,7 @@ describe('Squad specification suite v0.1', () => { expect(profile.capabilities).toEqual(manifest.capabilities); expect(manifest.revision.specification.artifact).toBe(profile.spec); expect(manifest.revision.evidence.artifact).toBe(profile.manifest); - expect(manifest.revision.specification.digest).toBe( + expect(manifest.revision.specification.digest, `${profile.spec}: exact-byte specification digest`).toBe( sha256(await readFile(path.join(ROOT, profile.spec))), ); const { revision: _manifestRevision, ...evidencePayload } = manifest;