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
176 changes: 176 additions & 0 deletions .agents/skills/markdown-frontmatter/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
---
name: markdown-frontmatter
description: Use when adding, fixing, or validating YAML frontmatter on managed Markdown governed by the project-standards Markdown Frontmatter Standard; covers structure, field values, id generation, and validation.
compatibility: Claude Code and Codex CLI
license: MIT
metadata:
author: Chris Purcell
version: '1.4'
---

# Markdown Frontmatter

## Overview

Author and fix YAML frontmatter for **managed Markdown documents** under the [project-standards Markdown Frontmatter Standard](https://github.com/L3DigitalNet/project-standards/blob/v5.10.0/standards/markdown-frontmatter/versions/1.6/README.md).

This skill ships with the standard package and is installed repo-local at `.agents/skills/markdown-frontmatter` when a repository adopts the standard. That path is deliberate: both Claude Code and Codex CLI can discover it without a global skill owner.

**Core principle: the schema is authoritative, not this file.** The machine contract is `markdown-frontmatter.schema.json` in project-standards, enforced by `project-standards validate`. This skill is the operating layer for the rules agents get wrong most often. On any conflict, the schema and current standard pages win.

## When to use

- Creating or editing a managed Markdown file (typically `README.md`, `docs/**/*.md`).
- A `project-standards validate`, `validate-frontmatter`, or `format-frontmatter --check` run failed and you need to fix the block.
- Deciding which `doc_type` / `status` / other controlled value to set.

**When NOT to use: files that must NEVER carry frontmatter.** Agent-instruction and agent-skill files are harness config, not managed documents: `CLAUDE.md`, `AGENTS.md`, and anything under `.claude/`, `.agents/`, `.codex/`. That includes this installed skill at `.agents/skills/markdown-frontmatter`. Exclude those paths through the package's `exclude` option in `.standards/config.toml` instead of adding metadata. A repo may also exclude its root `README.md` if it prefers no metadata table on its landing page.

## Required fields (the eleven)

Every managed document opens with a `---` fenced YAML block carrying at least these, in this order:

```yaml
---
schema_version: '1.1'
id: 'note-xxxxxx-human-title'
title: 'Human Title'
description: 'One-sentence description of the document.'
doc_type: 'note'
status: 'draft'
created: 'YYYY-MM-DD'
updated: 'YYYY-MM-DD'
tags: []
aliases: []
related: []
---
```

For most docs, add the standard-profile optionals after `updated` in canonical order: `reviewed` (date|null), `owner` (stable person/team/role), `consumer` (enum), then after `related`: `source` (array), `confidence` (enum), `visibility` (enum), `license` (string|null). Relationship fields are optional, used only when needed: `supersedes`, `superseded_by`, `depends_on`, `applies_to`.

## Formatting rules that actually fail validation

These are the machine-checked rules an agent skips by habit:

- **Quote every string, including dates.** `created: '2026-06-07'`, never `created: 2026-06-07`.
- **Identifier-like numbers are strings.** `schema_version: '1.1'`, not `1.1`.
- **Non-empty lists use block style** (`- 'item'` per line); **empty lists use `[]`**. No duplicate items.
- **No unknown top-level fields.** A stray `version:` or `type:` is rejected. Project- or tool-specific keys go under the `publish`, `project`, or `x_project` extension objects only.
- **Canonical key order** when keys are present:

```text
schema_version, id, title, description, doc_type, status, created, updated,
reviewed, owner, consumer, tags, aliases, related, supersedes, superseded_by,
depends_on, applies_to, source, confidence, visibility, license,
publish, project, x_project
```

## Controlled values

These fields accept only these values (the schema is the source of truth):

| Field | Allowed values |
| --- | --- |
| `doc_type` | `index`, `note`, `concept`, `reference`, `runbook`, `spec`, `plan`, `adr`, `decision`, `research`, `template`, `log`, `prompt`, `schema` |
| `status` | `draft`, `active`, `review`, `deprecated`, `archived`, `superseded`, `stub` |
| `confidence` | `high`, `medium`, `low`, `unknown` |
| `visibility` | `private`, `internal`, `public` |
| `consumer` | `user`, `agent`, `mix`, `unknown` |

- `README.md` and `index.md` → `doc_type: 'index'`. Files under `docs/research/` → `doc_type: 'research'`.
- `stub` is a **status**, never a `doc_type`. Use `doc_type`, never `type`.
- Canonical global tags include `frontmatter`, `metadata`, `standard`, `validation`, `infrastructure`, `it`, and `network`; repos may add documented local tags when the global set is insufficient.

## The `id` field — standard-enforced format

> **This is a standard rule, not a local addition.** `markdown-frontmatter@1.6` enforces the id format below via `validate-id` (run by `project-standards validate` and the V5 CI workflow). An id whose leading segment is not a valid `doc_type` **fails validation** with `prefix '<x>' is not a valid doc_type`. Earlier repo-name-prefixed ids no longer pass.

```text
{doc_type}-{base36-6}-{document-name}
```

The `doc_type` (one of the controlled values above), then a random 6-character base36 token, then a readable document slug, all lower kebab-case (e.g. `runbook-0f943i-restart-netbox-after-config-change`). The token keeps the id globally unique; the slug is frozen at creation and does **not** change when the title is edited.

**Generate the id with the script. Never invent the token yourself.** An LLM asked for a "random" base36 token produces low-entropy, collision-prone strings and reuses tokens already in context, defeating the uniqueness goal. `scripts/` is this skill's own directory (invoke by absolute path if your cwd is elsewhere):

```bash
scripts/new-doc-id <document-name> # bare id, doc_type 'note'
scripts/new-doc-id --doc-type runbook <document-name> # bare id, 'runbook' prefix
scripts/new-doc-id --scaffold --doc-type runbook <name> # full canonical frontmatter block
```

The `--doc-type` value becomes both the id prefix and (in `--scaffold`) the `doc_type` field, so the two always agree; it defaults to `note`. `--doc-type` and `--status` must be standard-controlled values. `--scaffold` emits the eleven required fields in canonical order with today's date correctly quoted. Replace the `REPLACE:` description placeholder before committing.

ADRs are the exception: follow the standard's ADR id form (`adr-{NNNN}-{repo-name}-{title}`, e.g. `adr-0001-homelab-use-postgresql-for-persistent-storage`), not the `doc_type`-prefixed format. Do not use the script for ADR ids.

## Worked example (compliant standard profile)

```yaml
---
schema_version: '1.1'
id: 'runbook-0f943i-restart-netbox-after-config-change'
title: 'Restart netbox after config change'
description: 'Procedure to safely reload netbox after editing its configuration.'
doc_type: 'runbook'
status: 'active'
created: '2026-03-10'
updated: '2026-06-07'
reviewed: '2026-06-07'
owner: 'platform-team'
consumer: 'user'
tags:
- 'infrastructure'
- 'network'
- 'operations'
- 'runbook'
aliases:
- 'netbox-restart'
related:
- 'docs/architecture.md'
source: []
confidence: 'high'
visibility: 'internal'
license: null
---
# Restart netbox after config change

...document body...
```

## Validate

Compliance = `project-standards validate` exits `0`. Run it from the repository root:

```bash
project-standards validate
```

That command runs schema validation, ID-format validation, and reference validation. Exit codes: `0` all matched files valid (or none matched); `1` one or more documents failed; `2` config/schema error.

Use the formatter check for canonical quote style, key order, and list layout:

```bash
format-frontmatter --check
```

To check or repair a single file's id: `validate-id <file>` (add `--fix` to rewrite an invalid id through the platform executor).

## Common mistakes

| Mistake | Fix |
| --- | --- |
| `type:` instead of `doc_type:` | Rename; `type` is not a field. |
| Unquoted date `created: 2026-06-07` | Quote it: `'2026-06-07'`. |
| `doc_type: 'readme'` for a README | README/index → `doc_type: 'index'`. |
| Extra top-level key (`version:`, `category:`) | Move under `project:`/`x_project:`, or drop it. |
| Frontmatter added to `CLAUDE.md` / `.claude/**` / `.agents/**` | Remove it; add the path to the package `exclude` option. |
| Omitting required arrays (`tags`/`aliases`/`related`) | Always present; empty = `[]`. |
| `doc_type: 'stub'` | `stub` is a `status`, not a `doc_type`. |

## Authoritative references

- [Standard README](https://github.com/L3DigitalNet/project-standards/blob/v5.10.0/standards/markdown-frontmatter/versions/1.6/README.md) — overview and adoption surface.
- [Structure Requirements](https://github.com/L3DigitalNet/project-standards/blob/v5.10.0/standards/markdown-frontmatter/versions/1.6/structure.md) — hard fields, key order, scalar/list rules, IDs, and validation.
- [Field Values](https://github.com/L3DigitalNet/project-standards/blob/v5.10.0/standards/markdown-frontmatter/versions/1.6/field-values.md) — lifecycle, ownership, canonical tags, aliases, relationships, sources, and extensions.
- [Adoption guide](https://github.com/L3DigitalNet/project-standards/blob/v5.10.0/standards/markdown-frontmatter/versions/1.6/adopt.md) — unified config, CI workflow, repo-local skill install, and compliance procedure.
- `standards/markdown-frontmatter/versions/1.6/schemas/markdown-frontmatter.schema.json` (in project-standards) — the selected package contract; wins on any conflict.
6 changes: 6 additions & 0 deletions .agents/skills/markdown-frontmatter/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
interface:
display_name: "Markdown Frontmatter"
short_description: "Create and maintain markdown frontmatter."

policy:
allow_implicit_invocation: true
198 changes: 198 additions & 0 deletions .agents/skills/markdown-frontmatter/scripts/new-doc-id
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
#!/usr/bin/env bash
# new-doc-id - generate a standard-conformant document id for the
# markdown-frontmatter skill: {doc_type}-{base36-6}-{document-name}
#
# This is the id format package 1.2 enforces via `validate-id`
# (run by `project-standards validate` and the V5 CI workflow): an id whose
# leading segment is not a valid doc_type is REJECTED. The prefix therefore
# is the doc_type, not the repo name (a repo-name prefix fails validation).
#
# The random token exists because LLMs cannot generate randomness: a model
# asked for a "random" base36 token produces low-entropy, collision-prone
# strings and tends to reuse tokens already in its context, defeating the
# global-uniqueness goal. /dev/urandom supplies real entropy.
#
# Usage: new-doc-id [--scaffold] [--doc-type TYPE] [--status S] <document-name>
# --doc-type TYPE doc_type - becomes the id prefix AND the scaffold's
# doc_type field (default: note). MUST be a valid
# doc_type or the id fails validate-id.
# --scaffold emit a full canonical-order frontmatter block (the
# eleven required fields, strings and dates quoted,
# empty lists as []) instead of the bare id
# --status S scaffold status (default: draft)
#
# The document name is sanitized with project_standards.id_format.slugify when
# available (spaces/punctuation collapse, Unicode is ASCII-normalized, long
# slugs are capped), with a stdlib fallback matching that algorithm. A trailing
# .md is dropped before slugging. Exit codes: 0 ok; 2 usage error.
#
# Requirements: bash, coreutils, Python 3.
# ADR ids are NOT this format - they follow the standard's ADR form
# adr-{NNNN}-{repo-name}-{title} (see SKILL.md); do not use this script for ADRs.

set -euo pipefail

usage() {
printf 'usage: new-doc-id [--scaffold] [--doc-type TYPE] [--status S] <document-name>\n' >&2
exit 2
}

valid_doc_type() {
case "$1" in
index | note | concept | reference | runbook | spec | plan | adr | decision | research | template | log | prompt | schema)
return 0
;;
*)
return 1
;;
esac
}

valid_status() {
case "$1" in
draft | active | review | deprecated | archived | superseded | stub)
return 0
;;
*)
return 1
;;
esac
}

SCAFFOLD=0
DOC_TYPE="note"
STATUS="draft"
DOC_NAME=""
while (($# > 0)); do
case "$1" in
--scaffold)
SCAFFOLD=1
shift
;;
--doc-type)
[[ $# -ge 2 ]] || usage
DOC_TYPE="$2"
shift 2
;;
--status)
[[ $# -ge 2 ]] || usage
STATUS="$2"
shift 2
;;
-*)
usage
;;
*)
[[ -z "$DOC_NAME" ]] || usage
DOC_NAME="$1"
shift
;;
esac
done
[[ -n "$DOC_NAME" ]] || usage
if ! valid_doc_type "$DOC_TYPE"; then
printf 'new-doc-id: invalid doc_type: %s\n' "$DOC_TYPE" >&2
exit 2
fi
if [[ "$DOC_TYPE" == "adr" ]]; then
printf 'new-doc-id: do not use this script for ADR ids; use adr-{NNNN}-{repo-name}-{title}\n' >&2
exit 2
fi
if ! valid_status "$STATUS"; then
printf 'new-doc-id: invalid status: %s\n' "$STATUS" >&2
exit 2
fi

SCRIPT_PATH="${BASH_SOURCE[0]}"
slug="$(
NEW_DOC_ID_SCRIPT="$SCRIPT_PATH" DOC_NAME="$DOC_NAME" python3 - <<'PY'
import os
import re
import sys
import unicodedata
from pathlib import Path


def fallback_slugify(text: str) -> str:
text = unicodedata.normalize("NFKD", text).encode("ascii", "ignore").decode("ascii")
text = text.lower()
text = re.sub(r"[^a-z0-9]+", "-", text).strip("-")
if len(text) > 60:
head = text[:60]
if "-" in head:
head = head[: head.rfind("-")]
text = head.strip("-")
return text


def load_slugify():
script = Path(os.environ["NEW_DOC_ID_SCRIPT"]).resolve()
candidates = [Path.cwd() / "src"]
candidates.extend(parent / "src" for parent in script.parents)
for candidate in candidates:
if (candidate / "project_standards" / "id_format.py").exists():
sys.path.insert(0, str(candidate))
break
try:
from project_standards.id_format import slugify
except Exception:
return fallback_slugify
return slugify


doc_name = os.environ["DOC_NAME"]
if doc_name.endswith(".md"):
doc_name = doc_name[:-3]
print(load_slugify()(doc_name))
PY
)"
if [[ -z "$slug" ]]; then
printf 'new-doc-id: document name sanitized to nothing: %q\n' "$DOC_NAME" >&2
exit 2
fi

# 6 chars of base36 from /dev/urandom. head-then-tr (not tr-then-head) so tr
# never takes a SIGPIPE under pipefail; loop in case a chunk yields fewer
# than 6 valid chars.
token=""
while ((${#token} < 6)); do
chunk="$(head -c 256 /dev/urandom | tr -dc '0-9a-z')"
token="${token}${chunk}"
done
token="${token:0:6}"

# The doc_type prefix is what validate-id keys on - a non-doc_type prefix fails.
id="${DOC_TYPE}-${token}-${slug}"

if ((SCAFFOLD == 0)); then
printf '%s\n' "$id"
exit 0
fi

# Scaffold: the eleven required fields in canonical order, quoted per the
# rules that actually fail validation (quoted dates, quoted identifier-like
# numbers, [] for empty lists). Title = slug in Title Case as a starting
# point; description is a placeholder the author must replace.
title="$(
SLUG="$slug" python3 - <<'PY'
import os

print(os.environ["SLUG"].replace("-", " ").title())
PY
)"
today="$(date +%F)"
cat <<EOF
---
schema_version: '1.1'
id: '$id'
title: '$title'
description: 'REPLACE: one-sentence description of the document.'
doc_type: '$DOC_TYPE'
status: '$STATUS'
created: '$today'
updated: '$today'
tags: []
aliases: []
related: []
---
EOF
12 changes: 12 additions & 0 deletions .github/workflows/validate-standards.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
jobs:
frontmatter:
name: Frontmatter
uses: L3DigitalNet/project-standards/.github/workflows/validate-markdown-frontmatter.yml@v5
name: Validate project standards
"on":
pull_request: {}
push:
branches:
- main
permissions:
contents: read
Loading