diff --git a/.changeset/formalize-charter-profile.md b/.changeset/formalize-charter-profile.md new file mode 100644 index 000000000..0e0de8705 --- /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, 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/.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/docs/specification/README.md b/docs/specification/README.md new file mode 100644 index 000000000..585d227e9 --- /dev/null +++ b/docs/specification/README.md @@ -0,0 +1,169 @@ +# Squad Specification + +**Core version:** 0.1 + +**Status:** Working Draft + +**Draft date:** 2026-09-25 + +**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 +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. + +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) + +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. + +## Maturity taxonomy + +The suite uses exactly five maturity values: + +| Maturity | Meaning | Claimable | +|---|---|---| +| `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 | + +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` | `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, 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) | +| `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 a reference fixture, not portable conformance evidence. +Charter v0.1 is the only registered profile with a manifest, conformance +classes, capability identifiers, and immutable claim revisions. + +## 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 requirements and reference 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. This composition diagram is informative: dependency edges do not confer +transitive capabilities or conformance claims. + +## 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. + +`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. 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. 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/artifact-inventory.md b/docs/specification/artifact-inventory.md new file mode 100644 index 000000000..3f5783f45 --- /dev/null +++ b/docs/specification/artifact-inventory.md @@ -0,0 +1,144 @@ +# Squad Artifact Inventory + +**Document class:** Informative + +**Audit date:** 2026-09-25 + +**Purpose:** Repository-derived traceability, profile planning, and +interoperability maturity evidence for issue #2069. + +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. + +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](../../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 | +| 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 | 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 | [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.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 | +| 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 + +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 plus an independently declared runtime capability. The +portable conformance manifest is governed by a language-neutral JSON Schema and +covers every normative diagnostic code. + +## Promotion roadmap + +The v0.1 suite establishes clean boundaries rather than claiming every surface +is equally mature. The next promotion sequence is: + +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 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, + 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. +- 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 new file mode 100644 index 000000000..0e4ca6ee9 --- /dev/null +++ b/docs/specification/automation-v0.1.md @@ -0,0 +1,88 @@ +# Squad Automation Profile v0.1 + +**Status:** Working Draft + +**Document class:** Non-normative requirements draft + +**Profile identifier:** `squad-automation/v0.1` + +**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` + +**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. Candidate operations and roles + +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. + +## 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. Promotion criteria + +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 new file mode 100644 index 000000000..4e52a0fb1 --- /dev/null +++ b/docs/specification/ceremony-v0.1.md @@ -0,0 +1,159 @@ +# Squad Ceremony Profile v0.1 + +**Status:** Working Draft + +**Document class:** Non-normative requirements draft + +**Profile identifier:** `squad-ceremony/v0.1` + +**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` + +**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. + +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 +reference, facilitator selector, participant selectors, cooldown policy, input +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 +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, +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. Candidate operations and roles + +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. + +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 +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 records. + +## 9. Promotion criteria + +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 new file mode 100644 index 000000000..391788d01 --- /dev/null +++ b/docs/specification/charter-v0.1.md @@ -0,0 +1,404 @@ +# Squad Charter Profile v0.1 + + + +**Status:** Working Draft + +**Document class:** Normative unless a section is marked informative + +**Profile identifier:** `squad-charter/v0.1` + +**Editor:** Squad project maintainers + +**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. + +**Immutable publication tag:** None; this working draft is not a publication + +**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 +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. 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. +- [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 + +The artifact path associates a charter with the charter artifact family. The +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`. 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. + +## 4. 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 +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. + +## 5. Deterministic Markdown subset + +Structural recognition uses these rules, in order: + +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 + 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. + +## 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. + +| 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` | 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` | 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. +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 +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. + +## 7. 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. 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. + +## 8. Extensions + +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. + +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: + +- 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. + +## 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. +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 | +| `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 heading; one diagnostic per heading | +| `SQC106` | warning | Noncanonical extension heading | +| `SQC107` | warning | Compatibility field present | +| `SQC108` | warning | Unknown or noncanonically ordered standard section | + +## 10. 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 | +| `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 +capability because running validation does not prove that parsing or editing +was exercised. + +## 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. + +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. 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 + +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. 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`, 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 + 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. +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 + +### 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. + +```ts +import { + CHARTER_CAPABILITIES, + parseCharterMarkdown, + serializeCharter, + validateCharterConformance, +} from '@bradygaster/squad-sdk/parsers'; + +const validation = validateCharterConformance(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); +``` + +## 14. Errata + +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/configuration-casting-v0.1.md b/docs/specification/configuration-casting-v0.1.md new file mode 100644 index 000000000..0e1eed343 --- /dev/null +++ b/docs/specification/configuration-casting-v0.1.md @@ -0,0 +1,124 @@ +# Squad Configuration and Casting Profile v0.1 + +**Status:** Working Draft + +**Document class:** Non-normative requirements draft + +**Profile identifier:** `squad-configuration-casting/v0.1` + +**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` + +**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, casting identity, and provenance are separate promotion units. +Casting aesthetics are an informative, independently versioned extension and +do not define authority. + +## 2. Configuration requirements + +`config.json` is authored configuration intent. Portable configuration covers +declared values, precedence, and source evidence. Runtime-only keys and +provider catalogs remain implementation policy. + +## 3. Casting identity requirements + +`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, +universe or naming source, status, creation time, and update time. + +## 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: + +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. + +## 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. + +## 7. Candidate operations and roles + +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. + +## 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 +namespaced extensions. Invalid joins, revision regression, duplicate persistent +names where uniqueness is required, or stale writes fail closed. + +## 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 +revision; a mismatch returns `stale-revision` without partial mutation. + +## 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. + +## 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 +capabilities. + +## 12. Promotion criteria + +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 new file mode 100644 index 000000000..967466159 --- /dev/null +++ b/docs/specification/coordination-handoff-v0.1.md @@ -0,0 +1,87 @@ +# Squad Coordination and Handoff Profile v0.1 + +**Status:** Working Draft + +**Document class:** Non-normative requirements draft + +**Profile identifier:** `squad-coordination-handoff/v0.1` + +**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` + +## 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 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 +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. Candidate operations and roles + +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. + +## 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 records rather +than inline them. Credentials, hidden prompts, and secret tool results must not +appear in portable envelopes. + +## 9. Promotion criteria + +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/core-v0.1.md b/docs/specification/core-v0.1.md new file mode 100644 index 000000000..fb7d5be91 --- /dev/null +++ b/docs/specification/core-v0.1.md @@ -0,0 +1,219 @@ +# Squad Specification Core v0.1 + +**Status:** Working Draft + +**Document class:** Normative unless a section is marked informative + +**Capability identifier:** `squad-core/v0.1` + +**Editor:** Squad project maintainers + +**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. + +**Immutable publication tag:** None; this working draft is not a publication + +**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. +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 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 + +- **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 + 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. +- **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. + +### 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. +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, 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. + +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 +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 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 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 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 + +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/docs/specification/execution-preferences-v0.1.md b/docs/specification/execution-preferences-v0.1.md new file mode 100644 index 000000000..87bab441d --- /dev/null +++ b/docs/specification/execution-preferences-v0.1.md @@ -0,0 +1,102 @@ +# Squad Execution Preferences Profile v0.1 + + + +**Status:** Working Draft + +**Document class:** Non-normative requirements draft + +**Profile identifier:** `squad-execution-preferences/v0.1` + +**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` + +## 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. Candidate operations and behavior classes + +Candidate operations 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. Promotion criteria + +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 new file mode 100644 index 000000000..713173768 --- /dev/null +++ b/docs/specification/github-work-lifecycle-binding-v0.1.md @@ -0,0 +1,194 @@ +# Squad GitHub Work Lifecycle Binding v0.1 + +**Status:** Working Draft + +**Document class:** Non-normative requirements draft + +**Binding identifier:** `squad-work-github/v0.1` + +**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` + +## 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 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 +checked at execution time. + +## 6. Revision tokens, idempotency, and stale writes + +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. + +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 does not silently treat label creation or authentication +failure as success. + +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 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. +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!2078", + "head": "c03ba950", + "state": "needs-review" +} +``` + +## 10. Promotion criteria + +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 new file mode 100644 index 000000000..deefa4300 --- /dev/null +++ b/docs/specification/governance-review-v0.1.md @@ -0,0 +1,162 @@ +# Squad Governance and Review Profile v0.1 + +**Status:** Working Draft + +**Document class:** Non-normative requirements draft + +**Profile identifier:** `squad-governance-review/v0.1` + +**Maturity:** Requirements draft; non-claimable + +**Claimable capabilities:** None + +**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. + +**Depends on:** `squad-core/v0.1`, `squad-interop/v0.1`, +`squad-team-routing/v0.1` + +**Protocol record:** governance evidence records + +## 1. Scope and non-goals + +**Section status:** Informative overview. + +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:** 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 +**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:** Informative reference shape. + +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. Schema +acceptance proves only that represented fields match this draft shape. + +## 4. State machine + +**Section status:** Informative transition design exercised by reference schema +checks. + +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` + +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:** Informative promotion requirements. + +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:** Informative relationship requirements. Current TypeScript +checks are reference-implementation checks, not portable conformance evidence. + +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 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`. + +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:** Informative promotion requirements. + +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:** Informative promotion requirements. Current schema and +TypeScript checks cover selected shapes and relationships only. + +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 + +**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 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 + +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 new file mode 100644 index 000000000..d9df95713 --- /dev/null +++ b/docs/specification/interoperability-conventions-v0.1.md @@ -0,0 +1,196 @@ +# Squad Interoperability Conventions v0.1 + + + +**Status:** Working Draft + +**Document class:** Non-normative requirements draft + +**Profile identifier:** `squad-interop/v0.1` + +**Maturity:** Requirements draft; non-claimable + +**Claimable capabilities:** None + +**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. + +**Depends on:** `squad-core/v0.1` + +## 1. Scope + +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. Every section is non-claimable until +portable manifests and cases are registered. + +## 2. Suite discovery and capability claims + +**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; +- 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. + +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. + +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. 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:** Informative reference shape exercised by +`evidence.schema.json` and implementation tests. + +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; +- 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. + +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 + +**Section status:** Informative future-profile requirements; the envelope +validates represented fields but does not execute retry or replay behavior. + +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 + +**Section status:** Informative security requirements and reference schema +shape pending portable mutation fixtures. + +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 +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. 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. 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 +second `#operation` alias. + +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`, +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 +reviewer before publication. Passing a schema alone does not prove executor or +authority conformance. + +## 10. Versioned references and precedents + +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 +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 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 + +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 requirements draft can advertise a capability. diff --git a/docs/specification/planning-activation-v0.1.md b/docs/specification/planning-activation-v0.1.md new file mode 100644 index 000000000..02e30495d --- /dev/null +++ b/docs/specification/planning-activation-v0.1.md @@ -0,0 +1,109 @@ +# Squad Planning and Activation Profile v0.1 + + + +**Status:** Working Draft + +**Document class:** Non-normative requirements draft + +**Profile identifier:** `squad-planning-activation/v0.1` + +**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`, +`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. Planning record envelope + +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 +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 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 +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 record +existence, model output, or a passing validator must not be treated as human +approval. + +PRD prose quality, completeness, and preferred decomposition style are +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 + +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. Candidate operations and roles + +Candidate operations are `planning.produce`, `planning.validate`, +`planning.accept-scope`, `planning.accept-implementation`, and +`planning.activate`. Producers create records; 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 planning records. + +## 10. Promotion criteria + +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 new file mode 100644 index 000000000..b770522b1 --- /dev/null +++ b/docs/specification/state-memory-requirements-v0.1.md @@ -0,0 +1,99 @@ +# 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 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` + +## 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. State authority requirements + +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; +- trust boundaries among repository state, host-injected services, and remote + providers; +- encryption, secrets, privacy, redaction, and data residency declarations. + +## 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 +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. + +## 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. + +## 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, +authorization, retention disclosure, deletion behavior, and secret filtering. +Recovery tools must not leak state from another repository, worktree, user, or +account. + +## 7. 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. 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. + +## 8. 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..1674e4060 --- /dev/null +++ b/docs/specification/team-routing-v0.1.md @@ -0,0 +1,117 @@ +# Squad Team and Routing Profile v0.1 + +**Status:** Working Draft + +**Document class:** Non-normative requirements draft + +**Profile identifier:** `squad-team-routing/v0.1` + +**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` + +**Artifacts:** `team.md`, `routing.md` + +**Feedback:** Issue #2069 + +## 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. 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 +existing charter whose identity matches the member ID. + +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 advertises no capability for this draft. + +## 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 + 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. + +## 6. Candidate operations and implementation roles + +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. + +## 7. 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. + +## 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. + +## 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 +closed on unsupported profile versions and unknown mandatory capabilities. + +## 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. + +## 11. Promotion criteria + +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 new file mode 100644 index 000000000..71386829d --- /dev/null +++ b/docs/specification/work-lifecycle-v0.1.md @@ -0,0 +1,125 @@ +# Squad Work Lifecycle Profile v0.1 + + + +**Status:** Working Draft + +**Document class:** Non-normative requirements draft + +**Profile identifier:** `squad-work-lifecycle/v0.1` + +**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`, +`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. 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 + +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. Candidate operations and roles + +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. + +## 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. Promotion criteria + +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-lock.json b/package-lock.json index c6ceb7723..9c4097136 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": "^5.0.2", + "ajv": "^8.20.0", "cspell": "^10.3.3", "eslint": "^10.10.0", "eslint-plugin-n": "^18.3.0", @@ -1556,24 +1557,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", @@ -3059,14 +3042,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", @@ -3088,24 +3072,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", @@ -4205,6 +4171,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, @@ -4224,6 +4207,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, @@ -4448,6 +4438,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" }, @@ -5235,8 +5227,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": { @@ -6875,6 +6868,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": { @@ -7747,6 +7742,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 8f9942097..e5f14b42d 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", @@ -51,6 +51,7 @@ "@typescript-eslint/eslint-plugin": "^8.70.0", "@typescript-eslint/parser": "^8.69.0", "@vitest/coverage-v8": "^5.0.2", + "ajv": "^8.20.0", "cspell": "^10.3.3", "eslint": "^10.10.0", "eslint-plugin-n": "^18.3.0", diff --git a/packages/squad-sdk/CHANGELOG.md b/packages/squad-sdk/CHANGELOG.md index 7f9c7f309..74809c1bc 100644 --- a/packages/squad-sdk/CHANGELOG.md +++ b/packages/squad-sdk/CHANGELOG.md @@ -1,5 +1,19 @@ # @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, explicit conformance profile selection, validation-before-compilation, + 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 ### Minor Changes diff --git a/packages/squad-sdk/src/agents/charter-compiler.ts b/packages/squad-sdk/src/agents/charter-compiler.ts index 58691474c..34322fbcc 100644 --- a/packages/squad-sdk/src/agents/charter-compiler.ts +++ b/packages/squad-sdk/src/agents/charter-compiler.ts @@ -8,8 +8,17 @@ 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'; +import { + CHARTER_PROFILE, + validateCharterMarkdown, + type CharterValidationResult, +} from './charter-validator.js'; /** Set form for fast lookup. */ const VALID_EFFORTS = new Set(VALID_REASONING_EFFORTS); @@ -25,8 +34,19 @@ 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`. 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) */ teamContext?: string; /** Routing rules content */ @@ -66,16 +86,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 +107,33 @@ 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; + /** 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[]; } /** @@ -107,6 +150,8 @@ export interface CompiledCharter extends SquadCustomAgentConfig { resolvedTools?: string[]; /** Parsed charter data */ parsed: ParsedCharter; + /** Validation performed before behavior-affecting fields were applied */ + validation: CharterValidationResult; } /** @@ -122,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 @@ -131,13 +179,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) { @@ -170,28 +243,41 @@ 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(', ')}` : `${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; @@ -208,6 +294,7 @@ export function compileCharterFull(options: CharterCompileOptions): CompiledChar resolvedContextTier, resolvedTools, parsed, + validation, }; } catch (error) { if (error instanceof ConfigurationError) throw error; @@ -231,7 +318,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 +328,199 @@ 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), + eol: scan.eol, + 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 { + 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; +} + /** * Capitalize first letter of a string. */ @@ -350,3 +528,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 new file mode 100644 index 000000000..7f53bb13c --- /dev/null +++ b/packages/squad-sdk/src/agents/charter-markdown.ts @@ -0,0 +1,234 @@ +/** + * 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 width: number; + readonly start: number; + readonly isExtension: boolean; +} + +export interface CharterMarkdownField { + readonly name: string; + readonly value: string; + readonly line: number; + readonly column: number; + readonly width: 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 h1s: readonly 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[] = []; + const h1s: CharterMarkdownHeading[] = []; + 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, + width: text.length, + start: sourceLine.start, + headingEnd: sourceLine.end - sourceLine.eol.length, + isExtension, + }; + if (isExtension) extensionTail = true; + } + continue; + } + + if (extensionTail) continue; + + 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; + const fieldMatch = text.match(FIELD); + if (fieldMatch) { + fields.push({ + name: fieldMatch[1]!.trim(), + value: fieldMatch[2]!, + line: sourceLine.line, + column: text.indexOf('**') + 1, + width: fieldMatch[1]!.trim().length + 5, + section: currentSection.name, + }); + } + } + + finishSection(markdown.length); + + return { + markdown, + eol: detectEol(markdown), + ...(h1 ? { h1 } : {}), + h1s, + 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..0f390dc5d --- /dev/null +++ b/packages/squad-sdk/src/agents/charter-validator.ts @@ -0,0 +1,579 @@ +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', + runtime: 'squad-charter/v0.1/runtime', +} 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', + 'SQC014', + 'SQC015', + 'SQC101', + 'SQC102', + 'SQC103', + 'SQC104', + 'SQC105', + 'SQC106', + 'SQC107', + 'SQC108', +] 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 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. */ + 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]+)*)$/; +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 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( + 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 { + 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, + 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 (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 "${selectedProfile}"`, + { line: 1 }, + ); + return createResult(selectedProfile, diagnostics, []); + } + + const scan = scanCharterMarkdown(markdown); + if (!markdown.trim()) { + add('SQC001', 'error', 'charter.md must not be empty', { line: 1 }); + 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)) { + add( + 'SQC003', + 'warning', + 'the canonical H1 form is "# — "', + headingLocation(scan.h1), + ); + } + + 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) { + 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 legacySections = scan.sections.filter(heading => + LEGACY_RESPONSIBILITY_SECTIONS.some(section => equalName(section, heading.name)), + ); + for (const legacySection of legacySections) { + add( + 'SQC105', + 'warning', + `legacy responsibility heading "${legacySection.name}" is consumer-compatible but is not canonical v0.1`, + headingLocation(legacySection), + ); + } + + 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( + '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), + ); + } + } + + 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); + 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(selectedProfile, 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.width, + }; +} + +function fieldLocation( + field: CharterMarkdownField, +): { line: number; column: number; width: number } { + return { + line: field.line, + column: field.column, + width: field.width, + }; +} + +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..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'; @@ -38,9 +39,28 @@ 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, + validateCharterConformance, + validateCharterMarkdown, + type CharterCapability, + type CharterConformance, + type CharterConformanceOptions, + type CharterDiagnostic, + type CharterDiagnosticCode, + type CharterDiagnosticSeverity, + type CharterValidationOptions, + type CharterValidationResult, +} from './charter-validator.js'; + // --- M1-9 Model Selection + M3-5 Model Fallback --- export { resolveModel, @@ -167,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)); @@ -197,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; @@ -267,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 c931674f7..ce12dc3b5 100644 --- a/packages/squad-sdk/src/parsers.ts +++ b/packages/squad-sdk/src/parsers.ts @@ -45,13 +45,44 @@ export { compileCharterFull, } from './agents/charter-compiler.js'; +export { + CHARTER_CAPABILITIES, + CHARTER_DIAGNOSTIC_CODES, + CHARTER_DIAGNOSTIC_SEVERITIES, + CHARTER_PROFILE, + CHARTER_PROFILE_METADATA, + validateCharterConformance, + validateCharterMarkdown, +} from './agents/charter-validator.js'; + +export type { + CharterCapability, + CharterConformance, + CharterConformanceOptions, + 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..4ef6a3d29 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,8 +43,25 @@ 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[] = []; + const eol = charter.source?.eol ?? '\n'; // Title line const name = charter.identity.name ?? 'Agent'; @@ -39,8 +69,8 @@ export function serializeCharter(charter: ParsedCharter): string { 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(''); } @@ -48,13 +78,22 @@ 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) { + if ( + charter.identity.expertise + && charter.identity.expertise.length > 0 + ) { lines.push(`- **Expertise:** ${charter.identity.expertise.join(', ')}`); } if (charter.identity.style) { @@ -62,24 +101,52 @@ 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(''); } + // Collaboration section + if (charter.collaboration !== undefined) { + lines.push(`## ${headings.collaboration}`); + lines.push(''); + lines.push(charter.collaboration); + 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 +158,20 @@ 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(''); - lines.push(charter.collaboration); - lines.push(''); - } - - return lines.join('\n').trimEnd() + '\n'; + const canonical = lines.join(eol).trimEnd() + eol; + const extensions = charter.source?.extensions.join('') ?? ''; + return extensions + ? `${canonical.trimEnd()}${eol}${eol}${extensions}` + : canonical; } diff --git a/packages/squad-sdk/src/types.ts b/packages/squad-sdk/src/types.ts index 2c5682656..03316d608 100644 --- a/packages/squad-sdk/src/types.ts +++ b/packages/squad-sdk/src/types.ts @@ -17,7 +17,17 @@ 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 { 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'; +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..6ab4a447c --- /dev/null +++ b/test-fixtures/spec/charter-v0.1/manifest.json @@ -0,0 +1,1292 @@ +{ + "$schema": "./manifest.schema.json", + "schemaVersion": 1, + "profile": "squad-charter/v0.1", + "revision": { + "specification": { + "artifact": "docs/specification/charter-v0.1.md", + "algorithm": "sha256", + "digest": "sha256:9bdc6440b83fbd087d7ac01129146dda067d86501b7d3ecf2e81134d5b7ce7c2" + }, + "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", + "squad-charter/v0.1/edit", + "squad-charter/v0.1/legacy-consume", + "squad-charter/v0.1/runtime" + ], + "cases": [ + { + "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": { + "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 + } + }, + { + "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", + "squad-charter/v0.1/runtime" + ], + "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"}, + {"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": { + "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": { + "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", + "squad-charter/v0.1/runtime" + ], + "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": "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 + } + }, + { + "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": { + "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 + } + }, + { + "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": { + "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": "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"} + ] + }, + "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 + } + }, + { + "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..28609070f --- /dev/null +++ b/test-fixtures/spec/charter-v0.1/manifest.schema.json @@ -0,0 +1,610 @@ +{ + "$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", "revision", "capabilities", "cases"], + "properties": { + "$schema": { + "const": "./manifest.schema.json" + }, + "schemaVersion": { + "const": 1 + }, + "profile": { + "const": "squad-charter/v0.1" + }, + "revision": { + "$ref": "#/$defs/revision" + }, + "capabilities": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/capability" + } + }, + "cases": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/case" + } + } + }, + "$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", + "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": { + "const": "conformance" + } + }, + "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-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-fixtures/spec/suite-v0.1/evidence.schema.json b/test-fixtures/spec/suite-v0.1/evidence.schema.json new file mode 100644 index 000000000..b31ed4a55 --- /dev/null +++ b/test-fixtures/spec/suite-v0.1/evidence.schema.json @@ -0,0 +1,468 @@ +{ + "$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"], + "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 }, + "basis": { "type": "string", "minLength": 1 }, + "policyRevision": { + "anyOf": [ + { "type": "string", "minLength": 1 }, + { "type": "integer" } + ] + } + } + }, + "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" } + }, + "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", + "currentRevisionAuthorUid", + "successorRevisionAuthorUid", + "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" }, + "currentRevisionAuthorUid": { "$ref": "#/$defs/uid" }, + "successorRevisionAuthorUid": { + "anyOf": [ + { "$ref": "#/$defs/uid" }, + { "type": "null" } + ] + }, + "reviewerUid": { "$ref": "#/$defs/uid" }, + "blockers": { + "type": "array", + "items": { "$ref": "#/$defs/blocker" } + }, + "lockoutUids": { + "type": "array", + "items": { "$ref": "#/$defs/uid" }, + "uniqueItems": true + }, + "eligibleRevisionAuthorUids": { + "type": "array", + "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"] } + }, + "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 new file mode 100644 index 000000000..5c21a85cd --- /dev/null +++ b/test-fixtures/spec/suite-v0.1/governance-rejection.example.json @@ -0,0 +1,66 @@ +{ + "specversion": "squad-interop/v0.1", + "schema": "squad-governance-review/v0.1", + "id": "1416ae81-91ef-48b3-b6fa-9b810b86b012", + "type": "squad.governance.rejected", + "source": "https://example.invalid/squad/governance", + "subject": "urn:squad:example:review-subject:change-request-42", + "time": "2026-09-25T14:17:33Z", + "actor": { + "uid": "urn:squad:example:actor:reviewer" + }, + "authority": { + "action": "review.reject", + "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": "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": "example-review:change-request-42:revision-1", + "replayKey": "example-provider-review:change-request-42:revision-1", + "payloadDigest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "redaction": "metadata-only", + "extensions": {}, + "data": { + "transition": { + "priorState": "under-review", + "currentState": "rejected" + }, + "reviewedSubject": { + "uid": "urn:squad:example:review-subject:change-request-42", + "revision": "urn:squad:example:revision:rejected-1" + }, + "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": "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", + "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 new file mode 100644 index 000000000..1420c9114 --- /dev/null +++ b/test-fixtures/spec/suite-v0.1/index.json @@ -0,0 +1,320 @@ +{ + "$schema": "./index.schema.json", + "suite": "squad-conformance/v0.1", + "schemaVersion": 1, + "publication": { + "status": "working-draft", + "revision": null, + "digest": null + }, + "dialects": [ + { + "id": "charter-v0-1", + "description": "Executable Charter v0.1 working-draft conformance manifest." + }, + { + "id": "normative-draft", + "description": "Normative working draft without standalone claimable capabilities." + }, + { + "id": "suite-v0-1", + "description": "Reference shared schemas and fixtures with no conformance claim." + }, + { + "id": "requirements-only", + "description": "Non-claimable conceptual coverage with explicit promotion criteria." + } + ], + "sharedSchemas": [ + "test-fixtures/spec/suite-v0.1/evidence.schema.json" + ], + "profiles": [ + { + "id": "squad-core/v0.1", + "maturity": "experimental-normative-draft", + "documentClass": "normative-profile", + "spec": "docs/specification/core-v0.1.md", + "revision": null, + "dialect": "normative-draft", + "dependencies": [], + "conformanceClasses": [], + "implementationRoles": ["producer", "consumer", "editor", "validator", "executor"], + "capabilities": [], + "manifest": 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": "requirements-draft", + "documentClass": "requirements", + "spec": "docs/specification/interoperability-conventions-v0.1.md", + "revision": null, + "dialect": "requirements-only", + "dependencies": ["squad-core/v0.1"], + "conformanceClasses": [], + "implementationRoles": ["producer", "consumer", "validator", "executor"], + "capabilities": [], + "manifest": null, + "manifestSchema": null, + "fixtures": [], + "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", + "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:9bdc6440b83fbd087d7ac01129146dda067d86501b7d3ecf2e81134d5b7ce7c2" + }, + "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"], + "implementationRoles": ["producer", "consumer", "editor", "validator", "executor"], + "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/runtime" + ], + "manifest": "test-fixtures/spec/charter-v0.1/manifest.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": "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": [], + "implementationRoles": ["producer", "consumer", "editor", "validator", "executor"], + "capabilities": [], + "manifest": 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": "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": [], + "implementationRoles": ["producer", "consumer", "editor", "validator", "executor"], + "capabilities": [], + "manifest": 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": "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": [], + "implementationRoles": ["producer", "consumer", "validator", "executor"], + "capabilities": [], + "manifest": null, + "manifestSchema": null, + "fixtures": [ + "test-fixtures/spec/suite-v0.1/governance-rejection.example.json" + ], + "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", + "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": [], + "implementationRoles": ["consumer", "validator", "executor"], + "capabilities": [], + "manifest": 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": "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": [], + "implementationRoles": ["producer", "consumer", "validator", "executor"], + "capabilities": [], + "manifest": 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": "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": [], + "implementationRoles": ["producer", "consumer", "validator", "executor"], + "capabilities": [], + "manifest": 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": "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": [], + "implementationRoles": ["producer", "consumer", "validator", "executor"], + "capabilities": [], + "manifest": 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": "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": [], + "implementationRoles": ["producer", "consumer", "validator", "executor"], + "capabilities": [], + "manifest": 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": "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": [], + "implementationRoles": ["producer", "consumer", "validator", "executor"], + "capabilities": [], + "manifest": 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": "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": [], + "implementationRoles": ["producer", "consumer", "validator", "executor"], + "capabilities": [], + "manifest": 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-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": [], + "implementationRoles": ["producer", "consumer", "validator", "executor"], + "capabilities": [], + "manifest": 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 new file mode 100644 index 000000000..05d21543e --- /dev/null +++ b/test-fixtures/spec/suite-v0.1/index.schema.json @@ -0,0 +1,272 @@ +{ + "$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-]+$" + }, + "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, + "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", + "documentClass", + "spec", + "revision", + "dialect", + "dependencies", + "conformanceClasses", + "implementationRoles", + "capabilities", + "manifest", + "manifestSchema", + "fixtures", + "implementationStatus", + "knownDeviations", + "promotionCriteria", + "entryPoint" + ], + "properties": { + "id": { "$ref": "#/$defs/profileId" }, + "maturity": { + "enum": [ + "executable-normative-draft", + "experimental-normative-draft", + "experimental-schema", + "requirements-draft", + "informative-draft" + ] + }, + "documentClass": { + "enum": [ + "normative-profile", + "normative-schema", + "requirements", + "informative" + ] + }, + "spec": { "$ref": "#/$defs/repoPath" }, + "revision": { + "anyOf": [ + { "$ref": "#/$defs/revision" }, + { "type": "null" } + ] + }, + "dialect": { "type": "string", "minLength": 1 }, + "dependencies": { + "type": "array", + "items": { "$ref": "#/$defs/profileId" }, + "uniqueItems": true + }, + "conformanceClasses": { + "type": "array", + "items": { + "enum": [ + "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" }, + "uniqueItems": true + }, + "manifest": { + "anyOf": [ + { "$ref": "#/$defs/repoPath" }, + { "type": "null" } + ] + }, + "manifestSchema": { + "anyOf": [ + { "$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": { + "revision": { "type": "null" }, + "capabilities": { "type": "array", "maxItems": 0 }, + "conformanceClasses": { "type": "array", "maxItems": 0 } + } + }, + "else": { + "properties": { + "revision": { "$ref": "#/$defs/revision" } + } + } + }, + { + "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/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 new file mode 100644 index 000000000..cc51240b6 --- /dev/null +++ b/test/charter-spec.test.ts @@ -0,0 +1,614 @@ +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, + CHARTER_DIAGNOSTIC_CODES, + CHARTER_PROFILE, + CHARTER_PROFILE_METADATA, + compileCharterFull, + parseCharterMarkdown, + serializeCharter, + validateCharterConformance, + validateCharterMarkdown, + type CharterCapability, + type CharterDiagnostic, + type ParsedCharter, +} from '@bradygaster/squad-sdk/parsers'; + +type ValidationApi = 'conformance'; +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 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; + classification: 'canonical' | 'noncanonical-compatible' | 'invalid'; + capabilities: CharterCapability[]; + diagnostics: ManifestDiagnostic[]; + }; + 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 { + $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[]; +} + +interface ManifestSchema extends Record { + $defs: { + capability: { enum: CharterCapability[] }; + diagnostic: { + properties: { + code: { enum: string[] }; + }; + }; + }; +} + +const FIXTURE_ROOT = path.resolve(__dirname, '..', 'test-fixtures', 'spec', 'charter-v0.1'); + +async function loadManifestDocument(): Promise { + return JSON.parse( + await readFile(path.join(FIXTURE_ROOT, 'manifest.json'), 'utf8'), + ) as FixtureManifest; +} + +async function loadManifestSchema(): Promise { + return JSON.parse( + 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'); + } + if (testCase.input.text !== null) return testCase.input.text; + throw new Error(`${testCase.id}: exactly one input source is required`); +} + +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 Reflect.apply(validateCharterConformance, undefined, [markdown, options]); +} + +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, + })); +} + +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, + }; +} + +function applyEdits(parsed: ParsedCharter, testCase: ManifestCase): void { + for (const edit of testCase.serialization?.edits ?? []) { + if (edit.field === 'identity.purpose') { + parsed.identity.purpose = edit.value; + } + } +} + +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'; +} + +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('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 { revision: _revision, ...missingRevision } = 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: '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: { + ...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 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) + .toEqual(CHARTER_DIAGNOSTIC_CODES); + expect(CHARTER_PROFILE_METADATA).toEqual({ + id: CHARTER_PROFILE, + artifactClass: 'required', + rootRelativePath: 'agents/{id}/charter.md', + 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 () => { + const manifest = await loadManifest(); + + for (const testCase of manifest.cases) { + const markdown = await readCaseSource(testCase); + const validation = validateCase(testCase, markdown); + expect( + { + 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); + } + + 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); + } + + 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', + charterContent: markdown, + ...(testCase.input.profile === null ? {} : { profile: testCase.input.profile }), + }); + + if (!runtimeExpectation.compile) { + let errorCodes: string[] = []; + try { + compile(); + } catch (error) { + errorCodes = runtimeErrorCodes(error); + } + expect(errorCodes, `${testCase.id}: runtime rejection codes`) + .toEqual(runtimeExpectation.errorCodes); + } else { + const compiled = compile(); + expect({ + resolvedModel: compiled.resolvedModel ?? null, + resolvedReasoningEffort: compiled.resolvedReasoningEffort ?? null, + resolvedContextTier: compiled.resolvedContextTier ?? null, + }, `${testCase.id}: runtime semantics`).toEqual({ + resolvedModel: runtimeExpectation.resolvedModel, + resolvedReasoningEffort: runtimeExpectation.resolvedReasoningEffort, + resolvedContextTier: runtimeExpectation.resolvedContextTier, + }); + } + } + } + }); +}); + +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: [], + }); + }); +}); 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', () => { diff --git a/test/specification-suite.test.ts b/test/specification-suite.test.ts new file mode 100644 index 000000000..867a06d6f --- /dev/null +++ b/test/specification-suite.test.ts @@ -0,0 +1,647 @@ +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'; +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 requirementsDocuments = [ + 'team-routing-v0.1.md', + 'configuration-casting-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', + 'state-memory-requirements-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; +} + +async function evidenceValidator(): Promise { + return createAjv().compile(await readJson('evidence.schema.json') as AnySchema); +} + +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 => { + 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 referenceGovernanceRelationshipErrors(value: { + causationId: string | null; + data: { + reviewerUid: string; + currentRevisionAuthorUid: string; + successorRevisionAuthorUid: string | null; + lockoutUids: string[]; + eligibleRevisionAuthorUids: string[]; + replayDisposition: string; + }; +}): string[] { + const errors: string[] = []; + 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'); + } + return errors; +} + +describe('Squad specification suite v0.1', () => { + it.each([ + ['index.schema.json', 'index.json'], + ['evidence.schema.json', 'governance-rejection.example.json'], + ])('validates reference %s fixtures', async (schemaFile, exampleFile) => { + 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('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('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; + 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('checks reviewer independence and revision-author eligibility in the reference implementation', async () => { + const example = await readJson('governance-rejection.example.json') as { + data: { + reviewerUid: string; + currentRevisionAuthorUid: string; + successorRevisionAuthorUid: string; + lockoutUids: string[]; + eligibleRevisionAuthorUids: string[]; + }; + }; + + 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(referenceGovernanceRelationshipErrors({ ...example, causationId: null })).toEqual([]); + }); + + 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; + data: { + reviewerUid: string; + currentRevisionAuthorUid: string; + successorRevisionAuthorUid: string; + lockoutUids: string[]; + eligibleRevisionAuthorUids: string[]; + replayDisposition: string; + }; + }; + const invalidLockout = clone(example); + invalidLockout.data.lockoutUids = []; + invalidLockout.data.eligibleRevisionAuthorUids = [invalidLockout.data.currentRevisionAuthorUid]; + const invalidReplay = clone(example); + invalidReplay.data.replayDisposition = 'duplicate-same-result'; + + expect(referenceGovernanceRelationshipErrors({ ...invalidLockout, causationId: null })).toEqual( + expect.arrayContaining(['author-not-locked', 'locked-author-eligible']), + ); + expect(referenceGovernanceRelationshipErrors({ ...invalidReplay, causationId: null })).toContain( + 'duplicate-without-original-link', + ); + }); + + 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<{ + 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[]; + 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)); + 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', + revision: null, + manifest: null, + capabilities: [], + conformanceClasses: [], + }); + 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 || + 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) { + expect(dialectIds.has(profile.dialect), 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); + expect(profile.implementationStatus, profile.id).not.toHaveLength(0); + 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([]); + 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; + revision: NonNullable; + capabilities: 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).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, `${profile.spec}: exact-byte 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); + 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) { + const document = JSON.parse(await readFile(path.join(ROOT, schema), 'utf8')) as AnySchema; + expect(() => createAjv().compile(document)).not.toThrow(); + } + }); + + 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[] }>; + }; + 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'); + + 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 () => { + 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('do not define authority'); + }); +});