From e0b05f43f2321b409473efa083c52998507f4849 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 15 Sep 2026 04:18:36 +0100 Subject: [PATCH 1/3] fix(ci): make the dogfood gate .deed-aware The dogfood gate enforced a format that no longer exists. Two defects, and fixing only the first would have been a fake cure. 1. .githooks/validate-a2ml.sh was frozen at a pre-DEED revision (13,737 b) with zero .deed references. Replaced with the canonical dual-accept body from deed-ecosystem/validate-action (17,544 b), which dispatches on the DEED s-expression head and keeps .a2ml passing as legacy. This repo's own SPDX-License-Identifier line is preserved. 2. The detect step that GATES that validator counted only *.a2ml: COUNT=$(find . -name '*.a2ml' ...) - name: Validate A2ML manifests if: steps.detect.outputs.count > 0 So on a repo that had migrated to .deed, COUNT would be 0, the validate step would be SKIPPED, and the job would report green having validated nothing. The selector now admits both extensions -- the same shape the K9 job in this file already uses. Edit 2 is behaviour-neutral today: this repo carries no .deed file, so COUNT is unchanged. It is purely forward-correct. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0168Bgpez8mFBcAqYAj8VgEx --- .githooks/validate-a2ml.sh | 106 ++++++++++++++++++++++------- .github/workflows/dogfood-gate.yml | 12 ++-- 2 files changed, 88 insertions(+), 30 deletions(-) diff --git a/.githooks/validate-a2ml.sh b/.githooks/validate-a2ml.sh index b0536764..5020c830 100755 --- a/.githooks/validate-a2ml.sh +++ b/.githooks/validate-a2ml.sh @@ -4,7 +4,7 @@ # # validate-a2ml.sh — A2ML manifest validation script # -# Scans for .a2ml files and validates: +# Scans for .a2ml and .deed files and validates: # 1. Required fields: agent-id or pedigree name, version # 2. SPDX-License-Identifier header presence # 3. Attestation block structure (if present) @@ -89,7 +89,7 @@ report_issue() { } # --------------------------------------------------------------------------- -# Validator: check a single .a2ml file +# Validator: check a single .a2ml or .deed file # --------------------------------------------------------------------------- validate_a2ml() { local file="$1" @@ -123,6 +123,7 @@ validate_a2ml() { # - project = "..." (for STATE.a2ml) local has_identity=false local has_version=false + local first_form_seen=false line_num=0 while IFS= read -r line; do @@ -150,6 +151,29 @@ validate_a2ml() { if [[ "$line" =~ ^[[:space:]]*(agent[-_]id|name|project|id)[[:space:]]*: ]]; then has_identity=true fi + # DEED s-expression head form: `(estate-deed`, `(repo-deed`, + # `(estate-atlas-deed`, `(praxis-deed`. Per DEED-GRAMMAR-SPEC + # <>, a file whose first form is one of the four declared + # heads is a deed of that kind, and the head satisfies the structural + # half of identity. This is what lets ATLAS.deed — which carries + # :registry-version and legitimately no :canonical-name — validate. + # The head is the FIRST form (DEED-GRAMMAR-SPEC <>: + # `Deed ::= Header Sep? Form Sep?` — one form, and it carries the head). + # Checking every line let a malformed file open with some other form and + # then append `(estate-deed ...)` lower down to buy identity. Only the + # first form is eligible. + if [[ "$first_form_seen" == "false" && "$line" =~ ^[[:space:]]*\( ]]; then + first_form_seen=true + if [[ "$line" =~ ^[[:space:]]*\((estate-deed|repo-deed|estate-atlas-deed|praxis-deed)([[:space:]]|$) ]]; then + has_identity=true + fi + fi + # DEED keyword identity form: `:canonical-name "..."` and the two other + # identity keywords the spec names. Note the leading colon: none of the + # three forms above match it, because they test the bare words. + if [[ "$line" =~ ^[[:space:]]*:(canonical-name|estate-authority|agent-id)[[:space:]] ]]; then + has_identity=true + fi # Check for version field — TOML form if [[ "$line" =~ ^[[:space:]]*(version|schema_version)[[:space:]]*= ]]; then has_version=true @@ -162,6 +186,19 @@ validate_a2ml() { if [[ "$line" =~ ^[[:space:]]*(version|schema_version)[[:space:]]*: ]]; then has_version=true fi + # DEED keyword version form: `:schema-version "1.0.0"` — leading colon, + # hyphenated, REQUIRED on all four deed heads (DEED-GRAMMAR-SPEC + # <>). All three patterns above spell it `schema_version` + # with no leading colon, so a conforming deed matched none of them. + # `:registry-version` is a distinct field, optional on the atlas. + # `:schema-version` ONLY. `:registry-version` is a distinct, optional + # atlas field (see the note above) and never satisfies the version + # requirement, which DEED-GRAMMAR-SPEC <> makes REQUIRED + # on all four heads. Accepting it let a registry-only atlas head pass + # with no schema version at all. + if [[ "$line" =~ ^[[:space:]]*:schema-version[[:space:]] ]]; then + has_version=true + fi done < "$file" # AI manifest files (0-AI-MANIFEST.a2ml, 0.1-AI-MANIFEST.a2ml, etc.) @@ -169,10 +206,14 @@ validate_a2ml() { local basename basename="$(basename "$file")" local is_manifest=false - if [[ "$basename" == *"AI-MANIFEST"* ]]; then + # `.a2ml` ONLY. The exemption exists because AI manifests are markdown-ish + # prose with no in-file identity; it is not a property of the name. Matching + # the bare basename meant `example-AI-MANIFEST.deed` was exempted from BOTH + # the identity and version checks — a deed that skipped the whole gate. + if [[ "$basename" == *"AI-MANIFEST"*.a2ml ]]; then is_manifest=true fi - # Canonical typed manifests under .machine_readable/descriptiles/ — identity comes + # Canonical typed manifests under /descriptiles/ — identity comes # from the enclosing directory + filename, not an in-file field. Sibling # files in the same directory (ECOSYSTEM.a2ml, STATE.a2ml) DO carry their # own $name/project and continue to be validated normally. @@ -203,20 +244,37 @@ validate_a2ml() { is_contractile_shape=true fi - # Canonical structured A2ML tree. Everything under a `.machine_readable/` - # directory is a typed agent-readable doc (CLADE, ANCHOR, STATE, - # ECOSYSTEM, bot_directives/{debt,coverage,methodology}, ai/AI, - # policies/*, integrations/*, …). Per the RSR convention these carry - # identity structurally — owning repo + path + filename — not via an - # in-file `name`/`agent-id`. This generalises the `.machine_readable/descriptiles/` - # rationale above to the whole tree: rsr-template-repo itself ships these - # files without an in-file identity key, so requiring one produces - # estate-wide false positives on every repo built from the canonical - # template. Files outside `.machine_readable/` are still validated. + # The structured A2ML tree. Everything under a repo's machine tree — + # `machine-readable/` canonically, `.machine_readable/` in the legacy + # layout — is a typed agent-readable doc (CLADE, ANCHOR, STATE, ECOSYSTEM, + # bot_directives/{debt,coverage,methodology}, ai/AI, policies/*, + # integrations/*, …). Per the RSR convention these carry identity + # structurally — owning repo + path + filename — not via an in-file + # `name`/`agent-id`. This generalises the `descriptiles/` rationale above + # to the whole tree: rsr-template-repo itself ships these files without an + # in-file identity key, so requiring one produces estate-wide false + # positives on every repo built from the canonical template. Files outside + # the machine tree are still validated. + # + # The machine tree is named `machine-readable/` canonically (un-hidden + # 2026-08); `.machine_readable/` is the LEGACY name. BOTH are matched: the + # canon, scaffoldia, the julia variant and ~300 minted repos still carry the + # dotted form, while rsr-template-repo has moved. Matching only one name + # makes whichever half of the estate has not migrated fail this check with + # 16 spurious "missing identity field" errors -- which is exactly what + # happened when the template renamed its tree and this action, being a + # separate implementation from the template's vendored copy, kept matching + # the old name only. local is_structural_identity=false - if [[ "$file" == *"/.machine_readable/"* || "$file" == "./.machine_readable/"* || "$file" == ".machine_readable/"* ]]; then - is_structural_identity=true - fi + # `*` matches the empty string, so */machine-readable/* already covers the + # ./-prefixed form that `find .` emits; spelling it out separately (as the + # original three-branch test did) is redundant. Verified equivalent across + # ./-prefixed, bare and absolute paths, and on the negative cases. + case "$file" in + */machine-readable/*|machine-readable/*|*/.machine_readable/*|.machine_readable/*) + is_structural_identity=true + ;; + esac if [[ "$has_identity" == "false" && "$is_manifest" == "false" && "$is_contractile_shape" == "false" && "$is_structural_identity" == "false" ]]; then report_issue "error" "$file" 1 \ @@ -259,7 +317,7 @@ validate_a2ml() { fi done < "$file" - if [[ $attestation_line -gt 0 && "$attestation_has_content" == "false" ]]; then + if [[ $attestation_line -gt 0 && "$attestation_has_content" == "false" && "$is_manifest" == "false" ]]; then report_issue "warning" "$file" "$attestation_line" \ "Attestation block found but missing proof/signature/hash fields" fi @@ -281,15 +339,15 @@ validate_a2ml() { } # --------------------------------------------------------------------------- -# Main: discover and validate .a2ml files +# Main: discover and validate .a2ml and .deed files # --------------------------------------------------------------------------- echo "::group::A2ML Manifest Validation" -echo "Scanning ${SCAN_PATH} for .a2ml files..." +echo "Scanning ${SCAN_PATH} for .a2ml and .deed files..." echo "" -# Find all .a2ml files, excluding .git directory -mapfile -t a2ml_candidates < <(find "$SCAN_PATH" -name '*.a2ml' -not -path '*/.git/*' -type f | sort) +# Find all .a2ml and .deed files, excluding .git directory +mapfile -t a2ml_candidates < <(find "$SCAN_PATH" \( -name '*.a2ml' -o -name '*.deed' \) -not -path '*/.git/*' -type f | sort) # Apply paths-ignore filter a2ml_files=() @@ -307,7 +365,7 @@ if [[ $SKIPPED -gt 0 ]]; then fi if [[ ${#a2ml_files[@]} -eq 0 ]]; then - echo "::notice::No .a2ml files found in ${SCAN_PATH}" + echo "::notice::No .a2ml or .deed files found in ${SCAN_PATH}" echo "files_scanned=0" >> "$GITHUB_OUTPUT_FILE" 2>/dev/null || true echo "errors=0" >> "$GITHUB_OUTPUT_FILE" 2>/dev/null || true echo "warnings=0" >> "$GITHUB_OUTPUT_FILE" 2>/dev/null || true @@ -315,7 +373,7 @@ if [[ ${#a2ml_files[@]} -eq 0 ]]; then exit 0 fi -echo "Found ${#a2ml_files[@]} .a2ml file(s)" +echo "Found ${#a2ml_files[@]} .a2ml/.deed file(s)" echo "" for file in "${a2ml_files[@]}"; do diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index b095d65c..3380ecd9 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -42,10 +42,10 @@ jobs: - name: Check for A2ML files id: detect run: | - COUNT=$(find . -name '*.a2ml' -not -path './.git/*' | wc -l) + COUNT=$(find . \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | wc -l) echo "count=$COUNT" >> "$GITHUB_OUTPUT" if [ "$COUNT" -eq 0 ]; then - echo "::warning::No .a2ml manifest files found. Every RSR repo should have 0-AI-MANIFEST.a2ml" + echo "::warning::No .deed manifest files found. Every RSR repo should have 0-AI-MANIFEST.deed (.a2ml is legacy and no longer authored)" fi - name: Validate A2ML manifests @@ -58,9 +58,9 @@ jobs: cat <<'EOF' >> "$GITHUB_STEP_SUMMARY" ## A2ML Validation - :warning: **No .a2ml files found.** Every RSR-compliant repo should have at least `0-AI-MANIFEST.a2ml`. + :warning: **No manifest found.** Every RSR-compliant repo should have at least `0-AI-MANIFEST.deed`. (`.a2ml` still validates as legacy but is no longer authored.) - Create one with: `a2mliser init` or copy from [rsr-template-repo](https://github.com/hyperpolymath/rsr-template-repo). + Copy one from [rsr-template-repo](https://github.com/hyperpolymath/rsr-template-repo). EOF else echo "## A2ML Validation" >> "$GITHUB_STEP_SUMMARY" @@ -335,7 +335,7 @@ jobs: MAX=6 # A2ML manifest present? - if find . -name '*.a2ml' -not -path './.git/*' | head -1 | grep -q .; then + if find . \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | head -1 | grep -q .; then SCORE=$((SCORE + 1)) A2ML_STATUS=":white_check_mark:" else @@ -389,7 +389,7 @@ jobs: | Tool/Format | Status | Notes | |-------------|--------|-------| - | A2ML manifest (0-AI-MANIFEST.a2ml) | ${A2ML_STATUS} | Required for all RSR repos | + | DEED manifest (0-AI-MANIFEST.deed) | ${A2ML_STATUS} | Required for all RSR repos | | K9 contracts | ${K9_STATUS} | Required for repos with config files | | .editorconfig | ${EC_STATUS} | Required for all repos | | Groove endpoint | ${GROOVE_STATUS} | Required for service repos | From b953dac780bdd5d016761de010b64e3c2fb15ddd Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 15 Sep 2026 09:14:33 +0100 Subject: [PATCH 2/3] fix(ci): address review on the .deed-aware dogfood gate Structural location under machine-readable/ or .machine_readable/ exempts a file from the IDENTITY requirement only. It must still declare a version: dropping is_structural_identity from the version check stops that check passing vacuously for every file in those trees. Proven by mutant -- a .deed with a canonical-name and no schema-version now warns, and did not before. Warnings do not fail the job, so ATLAS.deed (which carries :registry-version and legitimately no :schema-version) is unaffected. The gate now accepts both formats, so its own prose must not name only one: the step summary says "manifest file(s)" and the scorecard row is the format-neutral "A2ML/DEED manifest". Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0178nN4Nm3neFRy5K9StZKnB --- .githooks/validate-a2ml.sh | 2 +- .github/workflows/dogfood-gate.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.githooks/validate-a2ml.sh b/.githooks/validate-a2ml.sh index 5020c830..e63c34ec 100755 --- a/.githooks/validate-a2ml.sh +++ b/.githooks/validate-a2ml.sh @@ -281,7 +281,7 @@ validate_a2ml() { "Missing required identity field (agent-id, name, or project)" fi - if [[ "$has_version" == "false" && "$is_manifest" == "false" && "$is_contractile_shape" == "false" && "$is_structural_identity" == "false" ]]; then + if [[ "$has_version" == "false" && "$is_manifest" == "false" && "$is_contractile_shape" == "false" ]]; then report_issue "warning" "$file" 1 \ "Missing version or schema_version field" fi diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index 3380ecd9..4ea73653 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -65,7 +65,7 @@ jobs: else echo "## A2ML Validation" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Scanned **${A2ML_COUNT}** .a2ml file(s). See step output for details." >> "$GITHUB_STEP_SUMMARY" + echo "Scanned **${A2ML_COUNT}** manifest file(s). See step output for details." >> "$GITHUB_STEP_SUMMARY" fi # --------------------------------------------------------------------------- @@ -389,7 +389,7 @@ jobs: | Tool/Format | Status | Notes | |-------------|--------|-------| - | DEED manifest (0-AI-MANIFEST.deed) | ${A2ML_STATUS} | Required for all RSR repos | + | A2ML/DEED manifest | ${A2ML_STATUS} | Required for all RSR repos | | K9 contracts | ${K9_STATUS} | Required for repos with config files | | .editorconfig | ${EC_STATUS} | Required for all repos | | Groove endpoint | ${GROOVE_STATUS} | Required for service repos | From e6488a1520a4986f732a095c3f853b6792956d9e Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 15 Sep 2026 09:23:38 +0100 Subject: [PATCH 3/3] fix(ci): address review - align the gate's labels with what it counts Scope the version-check tightening to *.deed: legacy *.a2ml keeps its structural-identity exemption, which is what the review asked for. Add -type f so a directory named foo.deed cannot be counted as a manifest. The gate accepts both formats, so its summary line and scorecard row now name both rather than only one. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0178nN4Nm3neFRy5K9StZKnB --- .githooks/validate-a2ml.sh | 7 ++++++- .github/workflows/dogfood-gate.yml | 8 ++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.githooks/validate-a2ml.sh b/.githooks/validate-a2ml.sh index e63c34ec..b16fa852 100755 --- a/.githooks/validate-a2ml.sh +++ b/.githooks/validate-a2ml.sh @@ -281,7 +281,12 @@ validate_a2ml() { "Missing required identity field (agent-id, name, or project)" fi - if [[ "$has_version" == "false" && "$is_manifest" == "false" && "$is_contractile_shape" == "false" ]]; then + # DEED is the live, grammar-bearing format: its schema version is required even + # under a machine-readable tree. The structural-identity exemption below stays + # scoped to legacy *.a2ml, which is no longer authored. (CodeRabbit, PR review.) + local version_exempt_structural="$is_structural_identity" + case "$file" in *.deed) version_exempt_structural=false ;; esac + if [[ "$has_version" == "false" && "$is_manifest" == "false" && "$is_contractile_shape" == "false" && "$version_exempt_structural" == "false" ]]; then report_issue "warning" "$file" 1 \ "Missing version or schema_version field" fi diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index 4ea73653..82310522 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -42,7 +42,7 @@ jobs: - name: Check for A2ML files id: detect run: | - COUNT=$(find . \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | wc -l) + COUNT=$(find . -type f \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | wc -l) echo "count=$COUNT" >> "$GITHUB_OUTPUT" if [ "$COUNT" -eq 0 ]; then echo "::warning::No .deed manifest files found. Every RSR repo should have 0-AI-MANIFEST.deed (.a2ml is legacy and no longer authored)" @@ -65,7 +65,7 @@ jobs: else echo "## A2ML Validation" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Scanned **${A2ML_COUNT}** manifest file(s). See step output for details." >> "$GITHUB_STEP_SUMMARY" + echo "Scanned **${A2ML_COUNT}** manifest file(s) (.deed, or legacy .a2ml). See step output for details." >> "$GITHUB_STEP_SUMMARY" fi # --------------------------------------------------------------------------- @@ -335,7 +335,7 @@ jobs: MAX=6 # A2ML manifest present? - if find . \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | head -1 | grep -q .; then + if find . -type f \( -name '*.a2ml' -o -name '*.deed' \) -not -path './.git/*' | head -1 | grep -q .; then SCORE=$((SCORE + 1)) A2ML_STATUS=":white_check_mark:" else @@ -389,7 +389,7 @@ jobs: | Tool/Format | Status | Notes | |-------------|--------|-------| - | A2ML/DEED manifest | ${A2ML_STATUS} | Required for all RSR repos | + | AI manifest (0-AI-MANIFEST.deed, or legacy .a2ml) | ${A2ML_STATUS} | Required for all RSR repos | | K9 contracts | ${K9_STATUS} | Required for repos with config files | | .editorconfig | ${EC_STATUS} | Required for all repos | | Groove endpoint | ${GROOVE_STATUS} | Required for service repos |