agentleFS
jolarsjolars/panache/.agents/skills/add-syntax-construct/SKILL.md

add-syntax-construct

Skill246 starsChanged 22 days agoMITView on GitHub

Add a new block-level or inline-level syntax construct to Panache's

The file

Show its rules and text

We split this skill into the rules it gives an agent

13 rules. About 0 are common, 2 are distinctive, and 11 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 11

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Use this skill when adding syntax Panache does not yet understand, or when finishing a construct that was wired into the parser but not the …

    line 12
  2. Very specific:

    Not for lint rules: Use the add-lint-rule skill.

    line 22
  3. Very specific:

    Not for conformance triage: If the goal is "make example N pass", use commonmark-conformance or html-conformance instead — those start from …

    line 23
  4. Very specific:

    If the construct is Quarto- or MyST-specific and pandoc does not know it, find the closest pandoc construct it degrades to, and check …

    line 43
  5. Distinctive:

    Add a distinct kind for every byte category you need to round-trip: the marker, the delimiters, the content, and any attribute payload.

    line 50
  6. Very specific:

    Add the flag to Extensions in crates/panache-parser/src/options.rs.

    line 59
  7. Very specific:

    If the flag adds a config key, regenerate the schema: UPDATE_EXPECTED=1 cargo test config_schema.

    line 66
  8. Very specific:

    Add a module under crates/panache-parser/src/parser/blocks/ exporting try_parse_*() / emit_*().

    line 71
  9. Very specific:

    Add a module under crates/panache-parser/src/parser/inlines/ and hook it into inlines/core.rs.

    line 98
  10. Very specific:

    Check whether the surrounding dialect is already on the unified inline IR (inlines/inline_ir.rs) before choosing where to put the logic.

    line 101
  11. Very specific:

    Add one in crates/panache-parser/src/syntax/ if the linter or LSP needs to interrogate the node.

    line 108
  12. Distinctive:

    If a conformance suite regresses, the registry slot is the first suspect.

    line 152
  13. Very specific:

    Update the relevant page under docs/.

    line 156

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Use this skill when adding syntax Panache does not yet understand, or when finishing a construct that was wired into the parser but not the formatter.

This is the most error-prone change in the codebase. Skipping a step here does not fail loudly at the point of the mistake — it surfaces later as a losslessness failure, a silently unformatted node, or a global precedence regression in a conformance suite.

Scope boundaries

  • Not for lint rules. Use the add-lint-rule skill.
  • Not for conformance triage. If the goal is "make example N pass", use commonmark-conformance or html-conformance instead — those start from a failing case, not from a construct.
  • Not for inline IR migration. Moving an existing Pandoc inline onto the unified IR is pandoc-ir-migrate.

Step 1 — Confirm the pandoc-native shape first

Pandoc's AST decides what the CST must be able to express. Do not design the CST from the surface syntax alone.

printf '<your construct>' | pandoc -f markdown -t native
printf '<your construct>' | cargo run -- parse --to pandoc-ast

The second command prints the same shape as the first, so divergences diff directly. Note what pandoc nests and what it flattens — that is the structure the CST has to support, even if the surface syntax looks flat.

If the construct is Quarto- or MyST-specific and pandoc does not know it, find the closest pandoc construct it degrades to, and check assets/pandoc-spec/.

Step 2 — Add SyntaxKind variants

In crates/panache-parser/src/syntax/kind.rs, SCREAMING_SNAKE_CASE.

Add a distinct kind for every byte category you need to round-trip: the marker, the delimiters, the content, and any attribute payload. A construct that reuses TEXT for its markers cannot be formatted without re-lexing later, which is the usual reason a construct ends up needing a second pass.

Step 3 — Gate it behind a flag

New syntax almost always belongs to an extension or a flavor.

  1. Add the flag to Extensions in crates/panache-parser/src/options.rs.
  2. Wire the host-side default in src/config.rs.
  3. Set it per flavor.

A construct that is unconditionally live changes behavior for commonmark and will break that conformance suite. This is not optional.

If the flag adds a config key, regenerate the schema: UPDATE_EXPECTED=1 cargo test config_schema.

Step 4a — Block-level constructs

Add a module under crates/panache-parser/src/parser/blocks/ exporting try_parse_*() / emit_*(). Implement BlockParser in crates/panache-parser/src/parser/block_dispatcher.rs and insert it into the BlockParserRegistry::new() vector.

Registry order is precedence. The vector is deliberately aligned with pandoc's reader order, documented in a doc comment on the registry citing pandoc/src/Text/Pandoc/Readers/Markdown.hs:487-515. Placing a parser in the wrong slot is the single most common cause of subtly wrong output. Existing load-bearing constraints, each with a comment at its entry:

  • fenced code must precede YAML metadata
  • MyST directives must precede fenced code (brace-tagged opener wins)
  • close-parsers precede their open-parsers (fenced divs, MyST directives)
  • headings must precede horizontal rules
  • admonitions must precede indented code (4-space body would be eaten)
  • indented code must follow fenced code

Justify your position in a comment next to the entry, as the existing ones do.

detect_prepared() may return a payload (Box<dyn Any>) that parse_prepared() consumes, so emission never re-parses. Use it instead of scanning the line twice — re-scanning in emission is the quiet path back to two-pass parsing.

Step 4b — Inline-level constructs

Add a module under crates/panache-parser/src/parser/inlines/ and hook it into inlines/core.rs.

Check whether the surrounding dialect is already on the unified inline IR (inlines/inline_ir.rs) before choosing where to put the logic. The IR currently backs CommonMark only; the Pandoc dialect is mid-migration. Adding to the wrong side means the work is undone by the next migration step.

Step 5 — Typed AST wrapper

Add one in crates/panache-parser/src/syntax/ if the linter or LSP needs to interrogate the node. Consumers should never match on raw SyntaxKind sequences — that is what the wrappers exist to prevent.

Step 6 — Teach the formatter

Block dispatch is the match node.kind() in crates/panache-formatter/src/formatter/core.rs; inline handling lives in formatter/inline.rs.

An unhandled kind typically falls through to verbatim-ish output. That looks correct in a flat smoke test and breaks as soon as wrapping, indentation, or container nesting is involved — which is why step 7 insists on nested fixtures.

Step 7 — Fixtures in both suites

Both, not either:

  • Parser golden case in crates/panache-parser/tests/fixtures/cases/<name>/ (input.md + optional parser-options.toml) pins the CST via insta and proves losslessness.
  • Formatter golden case in tests/fixtures/cases/<name>/ (input.*, expected.*, optional panache.toml). Register the directory name in the golden_test_cases! macro at the bottom of tests/golden_cases.rs or it silently never runs.

Cover the construct nested inside a list item and inside a blockquote. Container prefixes are where most constructs break, and a top-level-only fixture will not catch it.

Step 8 — Verify

# Losslessness + idempotency on a real document using the construct
cargo run -- debug format --checks all scratch.qmd

# The real regression signal: a new block parser changes precedence globally
cargo test -p panache-parser --test commonmark
cargo test -p panache-parser --test pandoc

cargo test --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings

If a conformance suite regresses, the registry slot is the first suspect.

Step 9 — Document

Update the relevant page under docs/. If a config key was added, the schema regeneration from step 3 must be committed alongside.

Failure modes, in the order they usually appear

SymptomCause
Losslessness failureA byte category has no SyntaxKind; markers folded into TEXT
Construct parses, output unchangedFormatter match has no arm for the kind
Unrelated CommonMark examples regressRegistry slot too early, or the construct is not extension-gated
Breaks only inside a list or quoteNo nested fixture; container prefix handling untested
Formatting is not idempotentFormatter re-emits a marker the parser already captured

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.