Skip to content

Make resolve-frontmatter.sh emit the complete YAML frontmatter block #595

Description

@williamthorsen

Problem

After #537's resolve-frontmatter.sh refactor, every skill's Frontmatter resolution section still has the agent compose the YAML frontmatter from the script's JSON output plus per-skill literals (provenance.skill, provenance.isInteractive, optional model, optional extensions). The agent is responsible for parsing the JSON, mapping JSON fields to YAML keys, enforcing field-omission rules, composing the YAML in the correct field order, and quoting values correctly. This works, but it leaves a non-trivial composition surface in the agent. Schema changes require coordinated updates to prose at every site, and there's room for subtle mis-formatting (pr: null instead of omission; field reordering; mis-quoted ticket_ref: '#537').

Context

The script currently emits a JSON object with these keys (some optional): branch, commit, baseSha, pr, ticket_id, ticket_ref, platform, timestamp, run_id. Each calling skill is expected to translate that JSON into a YAML block via the partial-included instruction _partials/frontmatter-via-script.md plus per-skill bullets.

The next logical reduction is to have the script emit the complete YAML block — --- delimiters included — so the agent simply prepends the output to its artifact body. Field ordering, quoting, omission, and per-section grouping all become the script's responsibility, owned and tested in one place.

Issue #588 (DRY via markdown partials) was closed in favor of the approach in this ticket: once the agent stops composing YAML, the partials are unnecessary.

Solution

Extend resolve-frontmatter.sh so it emits a complete YAML frontmatter block (delimiters included) by default. Keep the existing JSON output as an explicit --format json escape hatch for the small number of bespoke sites whose composition needs exceed what a flag-driven CLI can express cleanly.

CLI surface

resolve-frontmatter.sh
  --skill NAME                  required for YAML mode; sets provenance.skill
  --interactive BOOL            required for YAML mode (true|false); sets provenance.isInteractive
  --model ID                    optional; sets provenance.model
  --extra KEY=VALUE             repeatable; scalar extension field
  --extra-list KEY=v1,v2,...    repeatable; list-of-scalars extension field
  --override KEY=VALUE          repeatable; replaces a script-resolved value (empty value = force-omit)
  --format yaml|json            default: yaml

Output structure (YAML mode)

The block is composed in three tiers, each in its prescribed order:

  1. provenance: block — skill, timestamp, baseSha (omit if unresolved), isInteractive, optional model. CamelCase keys per existing convention.
  2. Canonical top-level fields in schema order — ticket_id, ticket_ref, branch, commit, pr, run_id. Snake_case keys per existing convention. Optional fields are omitted (no null, no empty strings).
  3. Extension fields in caller-supplied (--extra / --extra-list) insertion order.

The script auto-quotes values containing YAML-special characters (e.g., # in ticket_ref, leading : in URLs). Lists emit in flow form: commits: [abc1234, def5678].

Override semantics

--override KEY=VALUE replaces the script's auto-resolved value for KEY. Empty value (--override run_id=) forces omission. Used by create-devlog to suppress the breadcrumb-derived run_id when no --run-id arg was supplied.

Bespoke exceptions

Two sites use --format json and compose YAML themselves:

  • refine-plan — computes the provenance: block from case branches on the input artifact's existing provenance (refinedBy, iteration, conditional preservation). The logic doesn't fit a flag-driven CLI without inventing a templating sub-language.
  • wrap-up (deferred-findings) — emits tickets_created as a list-of-objects extension ([{ id, items }]), which has no clean expression via --extra / --extra-list.

These exceptions are documented in artifact-conventions.md as a deliberate pattern, and each bespoke skill carries a one-sentence rationale at its frontmatter section.

Caller-side changes

For each in-scope skill or subagent:

  • Replace the existing partial-include and per-skill YAML-composition bullets with a single Bash invocation sentence.
  • Move the --extra / --extra-list arguments out of skill prose into the invocation line.

Inline ## Output format YAML frontmatter examples in skills and subagents are removed or replaced with a single reference to the canonical example in _data/artifact-conventions.md — schema documentation lives in one place.

The _partials/frontmatter-via-script.md partial is removed.

Acceptance criteria

  • resolve-frontmatter.sh accepts --skill, --interactive, --model, --extra KEY=VALUE (repeatable), --extra-list KEY=v1,v2,... (repeatable), --override KEY=VALUE (repeatable), and --format yaml|json (default yaml), and emits a complete YAML frontmatter block including --- delimiters in YAML mode.
  • The script enforces correct field order (provenance: block first, then canonical top-level fields in schema order, then extensions in insertion order) and omission rules (no pr: null, etc.).
  • The script auto-quotes values containing YAML-special characters; lists emit in flow form.
  • --override KEY= (empty value) forces field omission, overriding the script's auto-resolution.
  • --format json preserves the current JSON output behavior unchanged, including the existing field set and omission rules.
  • Shellspec tests cover: required-field presence in YAML mode, optional-field omission rules, --override force-omit, --extra / --extra-list insertion order, quoting of YAML-special characters, and preservation of existing JSON-mode behavior.
  • Each in-scope skill's Frontmatter resolution section is reduced to a single invocation sentence — no per-skill YAML composition prose remains.
  • Inline ## Output format YAML frontmatter examples in skills and subagents are removed or replaced with a single reference to the canonical example in _data/artifact-conventions.md.
  • _partials/frontmatter-via-script.md is removed.
  • refine-plan and wrap-up (deferred-findings) use --format json and compose YAML themselves; each carries a one-sentence rationale at its frontmatter section, and artifact-conventions.md documents the bespoke-exception pattern in one place.
  • No remaining inline YAML-composition prose in any artifact-writing skill's Frontmatter section apart from the two documented bespoke exceptions.
  • nmr --filter agents check passes (Prettier + tests + linting) after the refactor.
  • A representative end-to-end check across skill / subagent / orchestrated-subagent confirms the installed agent prepends the script's output without further composition.
  • New or modified behavior in this change is covered by tests.
  • Documentation, help text, and usage examples are updated to reflect the new CLI surface; references to anything removed (the partial, inline YAML examples) are deleted.

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureAdded or improved external functionalityscope:agents

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions