Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Each has a normative grammar in this repository:
|---|---|---|---|
| Repo facts: identity, state, meta, ecosystem, agentic, neurosym, playbook, and the `(bot-directives …)` and `(contractiles …)` clauses | `<repo>_chora.deed` at the repo root, one per repo (D312) | `1-formats/deed/spec/abnf/deed.abnf` (D308; prose: `1-formats/deed/spec/DEED-GRAMMAR-SPEC.adoc`) | `bun 1-formats/deed/tools/deed_lint.js FILE.deed` |
| Contracts | `*.k9.ncl` | `1-formats/k9/spec/K9-CONTRACT-SPEC.adoc` | `1-formats/k9/tools/k9-validate.sh --strict FILE` |
| Agent coordination | `coordination.k9` at the repo root: first line `K9!`, `schema_version: 1.1.1` | `2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.1.abnf` (prose: `2-protocols/k9-coordination/spec/COORDINATION-K9-SPEC.adoc`) | No validator script exists yet, and a generic YAML parser does not conform. The generator is `2-protocols/k9-coordination/tools/k9-init/`. |
| Agent coordination | `coordination.k9` at the repo root: first line `K9!`, `schema_version: 1.2.0` | `2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.2.abnf` (prose: `2-protocols/k9-coordination/spec/COORDINATION-K9-SPEC.adoc`) | No validator script exists yet, and a generic YAML parser does not conform. The generator is `2-protocols/k9-coordination/tools/k9-init/`. |

- **Translating old content.** The mapping specs in `1-formats/deed/mappings/`,
one per former A2ML family, decide where its content goes. Anything a
Expand Down
19 changes: 13 additions & 6 deletions .github/actions/signed-push/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ merge** (e.g. `mergeStateStatus: BLOCKED`). The only reliable way for automation
produce Verified commits is the GraphQL [`createCommitOnBranch`] mutation
authenticated as a **GitHub App installation** — GitHub signs those commits itself.

This action mints an App installation token and delegates the push to
[`Asana/push-signed-commits`], which diffs local vs. remote and replays the local
commits through `createCommitOnBranch`.
This action mints an App installation token and runs
[`push-verified.sh`](push-verified.sh) (bash, `gh` and `jq`), which diffs local
vs. remote and replays the local commits through `createCommitOnBranch`, oldest
first, each call naming the remote head it expects so that a concurrent push
fails closed. On success the local branch is reset to the remote.

## Prerequisites (one-time, account owner)

Expand Down Expand Up @@ -54,10 +56,15 @@ jobs:

## Notes

- Pinned to SHAs (`create-github-app-token` v3.2.0, `push-signed-commits` v1) per
estate supply-chain policy.
- `create-github-app-token` is SHA-pinned (v3.2.0) per estate supply-chain policy.
The push itself is `push-verified.sh`, so no other third-party action runs. (It
replaced `Asana/push-signed-commits`, a Python action with no licence file.)
- The remote branch must already exist: `createCommitOnBranch` commits onto an
existing ref.
- `createCommitOnBranch` cannot express merge commits, symlinks, submodules or file
modes, so a push containing any of them is refused before anything is sent, and
the pushed tree is checked against the local one.
- Run **after** local commits exist in the job — the action pushes the delta
between your local branch and its remote.

[`createCommitOnBranch`]: https://github.blog/changelog/2021-09-13-a-simpler-api-for-authoring-commits/
[`Asana/push-signed-commits`]: https://github.com/Asana/push-signed-commits
17 changes: 9 additions & 8 deletions .github/actions/signed-push/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ name: Signed push (verified commits via GitHub App)
description: >
Re-push the current branch's local commits as GitHub-"Verified" commits using a
GitHub App installation token and the createCommitOnBranch GraphQL mutation (via
Asana/push-signed-commits). This satisfies `required_signatures` branch rulesets
that a PAT or the default GITHUB_TOKEN cannot. Run this AFTER you have made local
commits in the job (checkout -> edit -> git commit -> this action).
push-verified.sh beside this file: bash, gh and jq, no third-party action). This
satisfies `required_signatures` branch rulesets that a PAT or the default
GITHUB_TOKEN cannot. Run this AFTER you have made local commits in the job
(checkout -> edit -> git commit -> this action).

inputs:
app-id:
Expand Down Expand Up @@ -39,8 +40,8 @@ runs:
# and auto-marks the resulting commits as "Verified". A PAT would leave them
# Unverified — which is why this must be an App token, not GITHUB_TOKEN/PAT.
- name: Push local commits as Verified
uses: Asana/push-signed-commits@d615ca88d8e1a946734c24970d1e7a6c56f34897 # v1.3
with:
github-token: ${{ steps.app-token.outputs.token }}
local_branch_name: ${{ steps.branch.outputs.name }}
remote_branch_name: ${{ steps.branch.outputs.name }}
shell: bash
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
BRANCH: ${{ steps.branch.outputs.name }}
run: bash "$GITHUB_ACTION_PATH/push-verified.sh" "$BRANCH"
138 changes: 138 additions & 0 deletions .github/actions/signed-push/push-verified.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MPL-2.0
#
# push-verified.sh — re-push a branch's local commits as GitHub-"Verified"
# commits through the GraphQL createCommitOnBranch mutation.
#
# Replaces Asana/push-signed-commits, which was Python (banned here), had no
# licence file, pulled the tag-pinned actions/setup-python@v2 and ran an
# unpinned `pip install requests` at run time. The contract is the same:
#
# * the remote branch must already exist and must not have diverged: every
# commit on it must already be in the local branch;
# * each local commit not on the remote is recreated, oldest first, with
# its message, by one createCommitOnBranch call that names the remote
# head it expects (expectedHeadOid), so a concurrent push fails closed;
# * on success the local branch is reset to the remote, whose commits now
# carry GitHub's authorship and verification.
#
# Two things are stricter than the Python action:
# * file contents are base64'd from the raw blob, so binary files survive
# (the Python action decoded them as UTF-8 text);
# * createCommitOnBranch can express neither file modes nor symlinks nor
# submodules, so a commit that adds or changes one is REFUSED rather than
# silently flattened, and the pushed tree is compared with the local one.
#
# Usage: push-verified.sh BRANCH
# Env: GH_TOKEN (App installation token), GITHUB_REPOSITORY (owner/repo),
# REMOTE (default origin), GH_BIN (default gh; the test suite stubs it).
set -euo pipefail

BRANCH="${1:?usage: push-verified.sh BRANCH}"
REMOTE="${REMOTE:-origin}"
GH_BIN="${GH_BIN:-gh}"
REPO="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY (owner/repo) is required}"

# die: report an error in Actions annotation form and stop.
die() { echo "::error::signed-push: $*" >&2; exit 1; }

WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT

git fetch --quiet "$REMOTE" "$BRANCH"
REMOTE_HEAD="$(git rev-parse "$REMOTE/$BRANCH")"
LOCAL_HEAD="$(git rev-parse --verify "refs/heads/$BRANCH")"

[ "$(git merge-base "$LOCAL_HEAD" "$REMOTE_HEAD")" = "$REMOTE_HEAD" ] \

Check failure on line 46 in .github/actions/signed-push/push-verified.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaE3UejhdrtYD-kgHOI7&open=AaE3UejhdrtYD-kgHOI7&pullRequest=1233
|| die "$REMOTE/$BRANCH has commits that $BRANCH lacks; refusing to push over them"

mapfile -t COMMITS < <(git rev-list --reverse "$REMOTE_HEAD..$LOCAL_HEAD")
if [ "${#COMMITS[@]}" -eq 0 ]; then

Check failure on line 50 in .github/actions/signed-push/push-verified.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaE3UejhdrtYD-kgHOI8&open=AaE3UejhdrtYD-kgHOI8&pullRequest=1233
echo "signed-push: $BRANCH has no commits that $REMOTE/$BRANCH lacks; nothing to push"
exit 0
fi

# changes_json: write the commit's file changes as {additions, deletions}
# JSON to $2. Renames arrive as a deletion plus an addition (--no-renames).
changes_json() {
local c="$1" out="$2" meta path old_mode new_mode new_blob status n=0
: > "$out.adds"; : > "$out.dels"
while IFS= read -r -d '' meta && IFS= read -r -d '' path; do
read -r old_mode new_mode _ new_blob status <<<"${meta#:}"
case "$status" in
D) jq -nc --arg p "$path" '{path: $p}' >> "$out.dels" ;;
A|M|T)
case "$new_mode" in
100644|100755) ;;
120000) die "$c adds or changes the symlink $path; createCommitOnBranch cannot express symlinks" ;;
160000) die "$c adds or changes the submodule $path; createCommitOnBranch cannot express submodules" ;;
*) die "$c gives $path the unsupported mode $new_mode" ;;
esac
if [ "$status" = A ] && [ "$new_mode" != 100644 ]; then

Check failure on line 71 in .github/actions/signed-push/push-verified.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaE3UejhdrtYD-kgHOI-&open=AaE3UejhdrtYD-kgHOI-&pullRequest=1233

Check failure on line 71 in .github/actions/signed-push/push-verified.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaE3UejhdrtYD-kgHOI9&open=AaE3UejhdrtYD-kgHOI9&pullRequest=1233
die "$c adds $path as executable ($new_mode); createCommitOnBranch would drop the mode"
fi
if [ "$status" != A ] && [ "$old_mode" != "$new_mode" ]; then

Check failure on line 74 in .github/actions/signed-push/push-verified.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaE3UejhdrtYD-kgHOJA&open=AaE3UejhdrtYD-kgHOJA&pullRequest=1233

Check failure on line 74 in .github/actions/signed-push/push-verified.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaE3UejhdrtYD-kgHOI_&open=AaE3UejhdrtYD-kgHOI_&pullRequest=1233
die "$c changes the mode of $path ($old_mode -> $new_mode); createCommitOnBranch cannot express modes"
fi
n=$((n + 1))
git cat-file blob "$new_blob" | base64 -w0 > "$out.blob.$n"
jq -nc --arg p "$path" --rawfile b "$out.blob.$n" '{path: $p, contents: $b}' >> "$out.adds"
;;
*) die "$c has an unsupported change ($status) to $path" ;;
esac
done < <(git diff --raw -z --no-renames --no-abbrev "$c^" "$c")
jq -n --slurpfile a "$out.adds" --slurpfile d "$out.dels" \
'{additions: $a, deletions: $d}' > "$out"
}

# Validate and prepare EVERY commit before sending any, so a commit that
# cannot be expressed refuses the whole push instead of leaving it partial.
i=0
for c in "${COMMITS[@]}"; do
[ "$(git rev-list --parents -n 1 "$c" | wc -w)" -eq 2 ] \

Check failure on line 92 in .github/actions/signed-push/push-verified.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaE3UejhdrtYD-kgHOJB&open=AaE3UejhdrtYD-kgHOJB&pullRequest=1233
|| die "$c is a merge commit; createCommitOnBranch can only create single-parent commits"
i=$((i + 1))
changes_json "$c" "$WORK/changes.$i.json"
done

expected="$REMOTE_HEAD"
pushed=0
i=0
for c in "${COMMITS[@]}"; do
i=$((i + 1))
jq -n \
--arg repo "$REPO" --arg branch "$BRANCH" --arg oid "$expected" \
--arg headline "$(git log -1 --format=%s "$c")" \
--arg body "$(git log -1 --format=%b "$c")" \
--slurpfile fc "$WORK/changes.$i.json" \
'{query: "mutation($in: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $in) { commit { oid } } }",
variables: {in: {
branch: {repositoryNameWithOwner: $repo, branchName: $branch},
expectedHeadOid: $oid,
message: {headline: $headline, body: $body},
fileChanges: $fc[0]}}}' > "$WORK/payload.json"
new="$("$GH_BIN" api graphql --input "$WORK/payload.json" --jq '.data.createCommitOnBranch.commit.oid' 2>"$WORK/gh.err" || true)"
if ! [[ "$new" =~ ^[0-9a-f]{40}$ ]]; then
msg="createCommitOnBranch failed for $c: $(tr '\n' ' ' < "$WORK/gh.err")"
if [ "$pushed" -gt 0 ]; then

Check failure on line 117 in .github/actions/signed-push/push-verified.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaE3UejhdrtYD-kgHOJC&open=AaE3UejhdrtYD-kgHOJC&pullRequest=1233
msg="$msg -- PARTIAL PUSH: $pushed of ${#COMMITS[@]} commit(s) are on $REMOTE/$BRANCH; reconcile by hand"
fi
die "$msg"
fi
expected="$new"
pushed=$((pushed + 1))
echo "signed-push: $c -> $new"
done

git fetch --quiet "$REMOTE" "$BRANCH"
[ "$(git rev-parse "$REMOTE/$BRANCH")" = "$expected" ] \

Check failure on line 128 in .github/actions/signed-push/push-verified.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaE3UejhdrtYD-kgHOJD&open=AaE3UejhdrtYD-kgHOJD&pullRequest=1233
|| die "$REMOTE/$BRANCH moved after the push (expected $expected)"
[ "$(git rev-parse "$REMOTE/$BRANCH^{tree}")" = "$(git rev-parse "$LOCAL_HEAD^{tree}")" ] \

Check failure on line 130 in .github/actions/signed-push/push-verified.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaE3UejhdrtYD-kgHOJE&open=AaE3UejhdrtYD-kgHOJE&pullRequest=1233
|| die "the pushed tree differs from the local one; $REMOTE/$BRANCH needs inspection"

if [ "$(git symbolic-ref --quiet --short HEAD || true)" = "$BRANCH" ]; then

Check failure on line 133 in .github/actions/signed-push/push-verified.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaE3UejhdrtYD-kgHOJF&open=AaE3UejhdrtYD-kgHOJF&pullRequest=1233
git reset --quiet --hard "$REMOTE/$BRANCH"
else
git branch --quiet --force "$BRANCH" "$REMOTE/$BRANCH"
fi
echo "signed-push: pushed $pushed commit(s) to $REMOTE/$BRANCH as Verified; $BRANCH now matches it"
4 changes: 2 additions & 2 deletions .meta/REQUIRED-FILES.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ reserved for Guix files (`guix.scm`, `.guix-channel`).
`1-formats/k9/tools/k9-validate.sh --strict FILE`.

| `coordination.k9` (repo root)
| Agent coordination rules. First line `K9!`, `schema_version: 1.1.1`.
| link:../2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.1.abnf[`coordination-k9-grammar_v1.1.abnf`]
| Agent coordination rules. First line `K9!`, `schema_version: 1.2.0`.
| link:../2-protocols/k9-coordination/spec/abnf/coordination-k9-grammar_v1.2.abnf[`coordination-k9-grammar_v1.2.abnf`]
(prose: link:../2-protocols/k9-coordination/spec/COORDINATION-K9-SPEC.adoc[COORDINATION-K9-SPEC]).
No validator script exists yet, and a generic YAML parser does not conform.
|===
Expand Down
50 changes: 25 additions & 25 deletions 1-formats/k9/IANA-MEDIA-TYPE-APPLICATION.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,19 @@

____
Prepared for submission to IANA per RFC 6838 (Vendor Tree) Submission
URL: https://www.iana.org/form/media-types Revision 3 – 2026-10-13
URL: https://www.iana.org/form/media-types Revision 4 – 2026-10-13
____

'''''

[IMPORTANT]
.Not filed: blocked on an open decision
[NOTE]
.File together with application/vnd.k9.coordination
====
Do not submit this application yet. Its *File extension(s)* field claims
`.k9`, and a second format, the k9-coordination dialect
(`coordination.k9`), uses that extension and the `K9!` magic with a
different body language. Whether that dialect gets its own subtype or a
parameter of this one has not been ruled on. See
<<open-decision,Open decision: coordination.k9>>.
The k9-coordination dialect (`coordination.k9`) shares the `K9!` magic but
is a different format, so it is registered as its own subtype,
link:../../2-protocols/k9-coordination/docs/iana/application-vnd.k9.coordination-registration.txt[`application/vnd.k9.coordination`]
(owner ruling, 2026-10-13). This application claims only `.k9.ncl`. See
<<companion-type,Companion type>>.
====

=== Important: Change from +nickel suffix
Expand Down Expand Up @@ -217,17 +216,18 @@ grammar of its own beyond the envelope. Standard Nickel tooling can parse
a K9 body only after the envelope-strip rule has removed the `+K9!+` line;
unstripped, `nickel typecheck` fails at line 1, column 3.

[[open-decision]]
==== Open decision: coordination.k9
[[companion-type]]
==== Companion type: application/vnd.k9.coordination

A second format, the k9-coordination dialect, also uses the `.k9`
extension and the `+K9!+` magic. Its body is a YAML-like language with its
own ABNF grammar, it never executes anything, and it is versioned
separately. Readers MUST classify a file by its first significant line
before validating it (K9 Component Contract Specification, K9-E005).
Whether that dialect is labelled with its own subtype or with a parameter
of this one has not been ruled on. Until it is, this application MUST NOT
be filed.
The k9-coordination dialect also uses the `+K9!+` magic. Its body is a
YAML-like language with its own ABNF grammar, it never executes anything,
and it is versioned separately, so it is registered as its own subtype
rather than as a parameter of this one (owner ruling, 2026-10-13).
Parameters are routinely dropped by file-type detectors, which would label
coordination files as Nickel components. Files carrying the magic MUST be
classified by their first significant line before validation (K9
Component Contract Specification, K9-E005); a coordination file is never
`application/vnd.k9` content.

'''''

Expand Down Expand Up @@ -299,9 +299,8 @@ signature verification and explicit user consent.
|Field |Value
|*Deprecated alias names* |None
|*Magic number(s)* |`+K9!+` (0x4B 0x39 0x21) at byte offset 0, followed by
LF. Shared with the k9-coordination dialect (see <<open-decision>>).
|*File extension(s)* |`+.k9.ncl+` (components); `+.k9+` (shared with the
k9-coordination dialect, see <<open-decision>>)
LF. Shared with `application/vnd.k9.coordination` (see <<companion-type>>).
|*File extension(s)* |`+.k9.ncl+` (`+.k9+` is `application/vnd.k9.coordination`)
|*Macintosh file type code(s)* |None
|*Object Identifier(s) / OID(s)* |None
|*Intended usage* |COMMON
Expand Down Expand Up @@ -364,7 +363,8 @@ https://github.com/hyperpolymath/standards/blob/main/1-formats/k9/SPEC.adoc
* [x] Remove unregistered +nickel suffix (RFC 6838 Section 4.2.8)
* [x] Unify file extensions under base type
* [x] Align with the K9 Component Contract Specification v1.0.0 (Revision 3)
* [ ] Settle the coordination.k9 open decision (subtype or parameter)
* [x] Settle the coordination.k9 question: own subtype, application/vnd.k9.coordination (Revision 4)
* [ ] File application/vnd.k9.coordination in the same submission round
* [ ] Submit via IANA web form at https://www.iana.org/form/media-types
* [ ] Monitor IANA email for review feedback
* [ ] Update specification with assigned media type upon approval
Expand All @@ -386,5 +386,5 @@ independent media types - Community consensus on suffix semantics

'''''

_Prepared: 2026-01-30_ _Revised: 2026-10-13 (Revision 3)_ _Status: Draft
– blocked on the coordination.k9 open decision_
_Prepared: 2026-01-30_ _Revised: 2026-10-13 (Revision 4)_ _Status: Draft
– ready for submission with application/vnd.k9.coordination_
35 changes: 25 additions & 10 deletions 1-formats/k9/SPEC.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -480,21 +480,29 @@ conformance by link:tools/k9-validate.sh[`tools/k9-validate.sh`].
<magic priority="50">
<match value="K9!" type="string" offset="0"/>
</magic>
<glob pattern="*.k9.ncl"/>
</mime-type>
<mime-type type="application/vnd.k9.coordination">
<comment>K9 Coordination File</comment>
<magic priority="60">
<match value="K9!" type="string" offset="0">
<match value="&#10;metadata:" type="string" offset="3:1024"/>
</match>
</magic>
<glob pattern="coordination.k9" weight="60"/>
<glob pattern="*.k9"/>
</mime-type>
----

Location: `/usr/share/mime/packages/k9.xml`

[WARNING]
====
The `*.k9` glob and the `K9!` magic also match `coordination.k9`, which is
a different format (the k9-coordination dialect, with a YAML-like body). This
entry therefore labels coordination files as Nickel components. How the
dialect should be labelled is an open decision that blocks the IANA filing.
See link:IANA-MEDIA-TYPE-APPLICATION.adoc#open-decision[the media type
application].
====
Both types open with `K9!`, so the magic alone cannot tell them apart.
The globs decide first (`*.k9.ncl` is a component, `*.k9` a coordination
file), and for a file without a recognised name the coordination entry adds
a nested match: a line starting `metadata:` within the first kilobyte, which
is how every coordination body begins and no Nickel component does. The
authoritative classification remains the K9-E005 discriminator in
link:spec/K9-CONTRACT-SPEC.adoc[the contract specification].

=== macOS (UTI)

Expand All @@ -506,15 +514,22 @@ application].
<string>application/vnd.k9</string>
----

The coordination type gets its own UTI, `org.k9-svc.coordination`, with
`public.mime-type` `application/vnd.k9.coordination`.

=== Minix

Static mapping in `/etc/mime.types`:

[source]
----
application/vnd.k9 k9
application/vnd.k9.coordination k9
----

`/etc/mime.types` keys on a file's last extension only, so it cannot express
`.k9.ncl` (that would be read as plain `.ncl`). Components are left to
content detection there.

== Philosophy

The `.k9` format treats documents as **Living Entities**.
Expand Down
Loading
Loading