From 3d80c079455b8570ba6eeedbd35a85b912bec591 Mon Sep 17 00:00:00 2001 From: hotragn Date: Wed, 30 Sep 2026 04:31:28 -0400 Subject: [PATCH] fix(markdown): seven agents' templates break out of their code blocks Markdown has no nesting at equal fence length. Inside a ```markdown template, a ```bash line is text, and the example's bare ``` closes the template. Seven agents nest example blocks inside a template that way, so on GitHub the rest of each template renders as headings and prose, and in three of them the last block then runs open to the end of the file: design-ux-architect code from line 414 to EOF ats-validator-architect code from line 307 to EOF master-plan-architect code from line 102 to EOF technical-writer, incident-response-commander, developer-advocate, workflow-architect templates end early, rest renders as markdown The fix for each is the one CommonMark intends: fence the outer template with four backticks so the three-backtick examples stay inside it. Nine outer blocks change; no content does. Nothing caught this because lib.sh's fence_closes_p accepted "```python" as a closing fence. CommonMark allows only whitespace after a closing run, and GitHub renders by that rule. With the old rule the linter and the OpenClaw split read those seven files one way and GitHub another, so each agreed with itself and all three disagreed with what users see. fence_closes_p now requires a bare run, and the OpenClaw split invariant in test-convert-outputs.sh uses the same rule. lint-agents.sh gets two errors, each with the file line number: - a fence line inside an open block that is long enough to close it but carries an info string -- the only way that happens is an attempted nest, and the message says how to fix it - a block that is still open at the end of the file The body loop now reads the file from the line after the frontmatter instead of from $body, which drops every "---" line and so cannot give a line number. It also keeps a last line with no trailing newline; two agents end on a closing fence with none, and without that they read as unclosed. The whole roster lints with 0 errors after the fix; on main the same check reports 18 errors across the seven files. scripts/test-lint-fences.sh covers both errors, the suggested fix, tilde fences, and fence_closes_p directly, and runs in check-tools.yml. --- .github/workflows/check-tools.yml | 3 + design/design-ux-architect.md | 4 +- .../engineering-ats-validator-architect.md | 4 +- ...engineering-incident-response-commander.md | 4 +- engineering/engineering-technical-writer.md | 8 +- scripts/lib.sh | 14 ++- scripts/lint-agents.sh | 32 ++++++- scripts/test-convert-outputs.sh | 7 +- scripts/test-lint-fences.sh | 87 +++++++++++++++++++ specialized/specialized-developer-advocate.md | 8 +- .../specialized-master-plan-architect.md | 4 +- specialized/specialized-workflow-architect.md | 4 +- 12 files changed, 153 insertions(+), 26 deletions(-) create mode 100755 scripts/test-lint-fences.sh diff --git a/.github/workflows/check-tools.yml b/.github/workflows/check-tools.yml index bd4bc2a0..800d9044 100644 --- a/.github/workflows/check-tools.yml +++ b/.github/workflows/check-tools.yml @@ -56,3 +56,6 @@ jobs: - name: Reject unclosed agent frontmatter run: bash scripts/test-frontmatter-closing.sh + + - name: Reject code fences that do not nest or never close + run: bash scripts/test-lint-fences.sh diff --git a/design/design-ux-architect.md b/design/design-ux-architect.md index 36e32434..efc80d93 100644 --- a/design/design-ux-architect.md +++ b/design/design-ux-architect.md @@ -326,7 +326,7 @@ grep -i "target\|audience\|goal\|objective" ai/memory-bank/site-setup.md ## 📋 Your Deliverable Template -```markdown +````markdown # [Project Name] Technical Architecture & UX Foundation ## 🏗️ CSS Architecture @@ -411,7 +411,7 @@ js/ **Foundation Date**: [Date] **Developer Handoff**: Ready for LuxuryDeveloper implementation **Next Steps**: Implement foundation, then add premium polish -``` +```` ## 💭 Your Communication Style diff --git a/engineering/engineering-ats-validator-architect.md b/engineering/engineering-ats-validator-architect.md index fe566833..b5c5230b 100644 --- a/engineering/engineering-ats-validator-architect.md +++ b/engineering/engineering-ats-validator-architect.md @@ -264,7 +264,7 @@ When performing an ATS audit or designing an ATS validation engine, you must pro ### Deliverable 5: Agent-Native Export Prompt -```markdown +````markdown ## 🤖 Prompt Pronto para Agentes Externos (Claude / ChatGPT / Cursor) ```markdown @@ -286,7 +286,7 @@ REGRAS RÍGIDAS: 3. Não exceda 30 palavras por bullet (evite sobrecarga cognitiva). 4. Retorne apenas os bullets reescritos formatados em Markdown. ``` -``` +```` ## 🔄 Your Workflow Process diff --git a/engineering/engineering-incident-response-commander.md b/engineering/engineering-incident-response-commander.md index 24819836..a81d2495 100644 --- a/engineering/engineering-incident-response-commander.md +++ b/engineering/engineering-incident-response-commander.md @@ -81,7 +81,7 @@ You are **Incident Response Commander**, an expert incident management specialis ``` ### Incident Response Runbook Template -```markdown +````markdown # Runbook: [Service/Failure Scenario Name] ## Quick Reference @@ -146,7 +146,7 @@ kubectl autoscale deployment/ -n production \ - Internal: Post update in #incidents Slack channel - External: Update [status page link] if customer-facing - Follow-up: Create post-mortem document within 24 hours -``` +```` ### Post-Mortem Document Template ```markdown diff --git a/engineering/engineering-technical-writer.md b/engineering/engineering-technical-writer.md index 447c306c..e17f72e5 100644 --- a/engineering/engineering-technical-writer.md +++ b/engineering/engineering-technical-writer.md @@ -53,7 +53,7 @@ You are a **Technical Writer**, a documentation specialist who bridges the gap b ## 📋 Your Technical Deliverables ### High-Quality README Template -```markdown +````markdown # Project Name > One-sentence description of what this does and why it matters. @@ -120,7 +120,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) ## License MIT © [Your Name](https://github.com/yourname) -``` +```` ### OpenAPI Documentation Example ```yaml @@ -203,7 +203,7 @@ paths: ``` ### Tutorial Structure Template -```markdown +````markdown # Tutorial: [What They'll Build] in [Time Estimate] **What you'll build**: A brief description of the end result with a screenshot or demo link. @@ -255,7 +255,7 @@ You built a [description]. Here's what you learned: - [Advanced tutorial: Add authentication](link) - [Reference: Full API docs](link) - [Example: Production-ready version](link) -``` +```` ### Docusaurus Configuration ```javascript diff --git a/scripts/lib.sh b/scripts/lib.sh index 6f76c1ef..0b9a6c28 100755 --- a/scripts/lib.sh +++ b/scripts/lib.sh @@ -77,17 +77,27 @@ fence_open_p() { } # fence_closes_p — 0 if -# closes the open fence (same char, run len >= open, indent <= open); 1 -# otherwise, including non-fence lines (callers need not pre-classify). +# closes the open fence (same char, run len >= open, indent <= open, nothing +# but whitespace after the run); 1 otherwise, including non-fence lines +# (callers need not pre-classify). +# +# The "nothing after the run" part is CommonMark's rule, and GitHub renders by +# it: inside an open ``` block, a "```python" line is content, not a closer +# and not a nested opener. Accepting it as a closer made these helpers read a +# ```markdown template holding a ```bash example as two short blocks, while +# GitHub saw one block that closed at the example's bare ``` — so a ## line +# the split treated as code rendered as a heading, and the reverse. fence_closes_p() { local line="$1" open_marker="$2" open_len="$3" open_indent="$4" local re='^( {0,3})(`{3,}|~{3,})' [[ "$line" =~ $re ]] || return 1 local close_indent=${#BASH_REMATCH[1]} local close_run="${BASH_REMATCH[2]}" + local rest="${line:${#BASH_REMATCH[0]}}" [[ "${close_run:0:1}" == "$open_marker" ]] || return 1 (( ${#close_run} >= open_len )) || return 1 (( close_indent <= open_indent )) || return 1 + [[ -z "${rest//[[:space:]]/}" ]] || return 1 return 0 } diff --git a/scripts/lint-agents.sh b/scripts/lint-agents.sh index 442e6e31..68e6fbde 100755 --- a/scripts/lint-agents.sh +++ b/scripts/lint-agents.sh @@ -162,8 +162,15 @@ lint_file() { local soul_headers=0 local agents_headers=0 - local fence_marker="" fence_len=0 fence_indent=0 - while IFS= read -r line; do + local fence_marker="" fence_len=0 fence_indent=0 fence_line=0 + # Walk the body from the file itself rather than from $body, so fence + # errors can name a real line number. ($body drops every "---" line.) + local lineno + lineno=$(awk 'NR > 1 && $0 == "---" {print NR; exit}' "$file") + # "|| [[ -n $line ]]" keeps a last line that has no trailing newline; several + # agents end on a closing fence with none, and dropping it reads as unclosed. + while IFS= read -r line || [[ -n "$line" ]]; do + lineno=$((lineno + 1)) # Skip fenced code blocks so ## doc-comment lines (e.g. GDScript `##`) # and in-fence markdown headers aren't miscounted (issue #849). if [[ -n "$fence_marker" ]]; then @@ -171,6 +178,17 @@ lint_file() { fence_marker="" fence_len=0 fence_indent=0 + elif fence_open_p "$line" \ + && [[ "${BASH_REMATCH[2]:0:1}" == "$fence_marker" ]] \ + && (( ${#BASH_REMATCH[2]} >= fence_len )); then + # A fence line long enough to close this block that did not close it + # can only be one with an info string: someone nesting ```bash inside + # a ```markdown template. Markdown has no nesting at equal length — + # GitHub shows that line as text and ends the outer block at the next + # bare ```, so the rest of the template renders as a document. + echo "ERROR $file:$lineno: '${line}' inside the block opened at line $fence_line does not nest — the next bare ${fence_marker}${fence_marker}${fence_marker} closes the outer block instead" + echo " fence the outer block with a longer run (e.g. ${fence_marker}${fence_marker}${fence_marker}${fence_marker}markdown ... ${fence_marker}${fence_marker}${fence_marker}${fence_marker}) so the inner ones stay inside it" + errors=$((errors + 1)) fi continue fi @@ -178,6 +196,7 @@ lint_file() { fence_marker="${BASH_REMATCH[2]:0:1}" fence_len=${#BASH_REMATCH[2]} fence_indent=${#BASH_REMATCH[1]} + fence_line=$lineno continue fi if [[ "$line" =~ ^##[[:space:]] ]]; then @@ -191,7 +210,14 @@ lint_file() { agents_headers=$((agents_headers + 1)) fi fi - done <<< "$body" + done < <(tail -n +"$((lineno + 1))" "$file") + + # An unclosed block runs to the end of the file: on GitHub, and in every + # tool the converters feed, everything after the opener renders as code. + if [[ -n "$fence_marker" ]]; then + echo "ERROR $file:$fence_line: code block is never closed — everything after line $fence_line renders as code" + errors=$((errors + 1)) + fi if [[ $soul_headers -eq 0 ]]; then echo "WARN $file: no section headers map to SOUL.md in convert.sh" diff --git a/scripts/test-convert-outputs.sh b/scripts/test-convert-outputs.sh index 95bc6b88..46e2dbbc 100755 --- a/scripts/test-convert-outputs.sh +++ b/scripts/test-convert-outputs.sh @@ -330,7 +330,7 @@ elif not colour_bad: # block in half and leaves each file holding a dangling fence, which renders as # broken markdown for every user of that integration (#849). So every fenced # block in a source must land intact in exactly one of the two files. -SPLIT_FENCE = re.compile(r"^(`{3,}|~{3,})") +SPLIT_FENCE = re.compile(r"^(`{3,}|~{3,})(.*)$") def body_lines(text): """Mirror lib.sh's get_body, including `$(...)`'s trailing-newline strip.""" @@ -352,10 +352,11 @@ def fence_blocks(lines): m = SPLIT_FENCE.match(line) if not m: continue - tok = m.group(1) + tok, rest = m.group(1), m.group(2) if not marker: marker, mlen, start = tok[0], len(tok), i - elif tok[0] == marker and len(tok) >= mlen: + elif tok[0] == marker and len(tok) >= mlen and not rest.strip(): + # only a bare run closes: "```bash" inside a block is content (lib.sh fence_closes_p) res.append((start, i)); marker, mlen, start = "", 0, None if marker and start is not None: res.append((start, len(lines) - 1)) diff --git a/scripts/test-lint-fences.sh b/scripts/test-lint-fences.sh new file mode 100755 index 00000000..cc44eab7 --- /dev/null +++ b/scripts/test-lint-fences.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Code fences that GitHub renders differently from what the author meant must +# fail lint, and the fix the error message suggests must pass it. +# +# Markdown has no nesting at equal fence length. Inside a ```markdown template, +# a ```bash line is text, and the example's closing ``` ends the template, so +# the rest of it renders as headings and prose. Seven roster agents shipped +# that way, three of them with the last block left open to the end of the file. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FIXTURE="$(mktemp -d "${TMPDIR:-/tmp}/agency-lint-fences.XXXXXX")" +trap 'rm -rf "$FIXTURE"' EXIT + +fail() { echo "FAIL: $*" >&2; exit 1; } + +frontmatter() { + cat <<'EOF' +--- +name: Fence Fixture +description: Fixture agent for the code-fence lint rules +color: blue +--- +## Identity +Enough words to clear the short-body warning, repeated so the linter has a +body to read: this fixture exists to check how fenced code blocks are parsed. +## Core Mission +Show a template that contains a code example. +## Critical Rules +Keep fences balanced. +EOF +} + +# 1. ```bash inside ```markdown: the inner fence does not nest. +{ frontmatter; cat <<'EOF' +### Template +```markdown +# Report +```bash +npm test +``` +## Findings +``` +EOF +} > "$FIXTURE/nested.md" +if bash "$SCRIPT_DIR/lint-agents.sh" "$FIXTURE/nested.md" > "$FIXTURE/nested.log" 2>&1; then + fail "linter accepted a \`\`\`bash fence nested in a \`\`\`markdown block of the same length" +fi +grep -Fq "nested.md:16: '\`\`\`bash' inside the block opened at line 14 does not nest" "$FIXTURE/nested.log" \ + || { cat "$FIXTURE/nested.log" >&2; fail "nested-fence error is missing or names the wrong lines"; } + +# 2. A block left open to the end of the file. +{ frontmatter; printf '%s\n' '### Layout' '```' 'css/' 'js/'; } > "$FIXTURE/unclosed.md" +if bash "$SCRIPT_DIR/lint-agents.sh" "$FIXTURE/unclosed.md" > "$FIXTURE/unclosed.log" 2>&1; then + fail "linter accepted a code block that is never closed" +fi +grep -Fq "unclosed.md:14: code block is never closed" "$FIXTURE/unclosed.log" \ + || { cat "$FIXTURE/unclosed.log" >&2; fail "unclosed-fence error is missing or names the wrong line"; } + +# 3. The fix the message suggests — a longer outer fence — passes, and so do +# tilde fences and a shorter run inside a longer block. +{ frontmatter; cat <<'EOF' +### Template +````markdown +# Report +```bash +npm test +``` +## Findings +```` +~~~text +```python is just text in here +~~~ +EOF +} > "$FIXTURE/valid.md" +bash "$SCRIPT_DIR/lint-agents.sh" "$FIXTURE/valid.md" > "$FIXTURE/valid.log" 2>&1 \ + || { cat "$FIXTURE/valid.log" >&2; fail "linter rejected correctly nested fences"; } + +# 4. The helper itself, which the OpenClaw split in convert.sh shares: only a +# bare run of the same character, at least as long, closes a block. +. "$SCRIPT_DIR/lib.sh" +fence_closes_p '```python' '`' 3 0 && fail "fence_closes_p treated '\`\`\`python' as a closing fence" +fence_closes_p '```' '`' 3 0 || fail "fence_closes_p rejected a bare closing fence" +fence_closes_p '```` ' '`' 3 0 || fail "fence_closes_p rejected a longer closing fence with trailing spaces" +fence_closes_p '```' '`' 4 0 && fail "fence_closes_p let a shorter run close a longer fence" + +echo "PASS: nested and unclosed fences are rejected; correctly nested fences pass" diff --git a/specialized/specialized-developer-advocate.md b/specialized/specialized-developer-advocate.md index e38d30a9..54255dc5 100644 --- a/specialized/specialized-developer-advocate.md +++ b/specialized/specialized-developer-advocate.md @@ -95,7 +95,7 @@ You are a **Developer Advocate**, the trusted engineer who lives at the intersec ``` ### Viral Tutorial Structure -```markdown +````markdown # Build a [Real Thing] with [Your Platform] in [Honest Time] **Live demo**: [link] | **Full source**: [GitHub link] @@ -144,7 +144,7 @@ Ready to go further? - → [Add authentication to your dashboard](link) - → [Deploy to production on Vercel](link) - → [Explore the full API reference](link) -``` +```` ### Conference Talk Proposal Template ```markdown @@ -179,7 +179,7 @@ Why this speaker: relevant experience and credibility signal.] ``` ### GitHub Issue Response Templates -```markdown +````markdown Thanks for the detailed report and reproduction case — that makes debugging much faster. @@ -206,7 +206,7 @@ likelihood/priority]. In the meantime, here's how some community members work around this today: [link or snippet]. -``` +```` ### Developer Survey Design ```javascript diff --git a/specialized/specialized-master-plan-architect.md b/specialized/specialized-master-plan-architect.md index 28f4b03a..7d641e5d 100644 --- a/specialized/specialized-master-plan-architect.md +++ b/specialized/specialized-master-plan-architect.md @@ -62,7 +62,7 @@ You operate under a strict **Zero Code Execution** guardrail: you design the blu ### The 5-Part Standard Implementation Plan Schema (`.md`) -```markdown +````markdown # 🏛️ [Project/Module Name] — Architectural Blueprint & Governance Plan ## 1. 🎓 Conceptual Masterclass: Philosophy, First Principles & Landscape @@ -99,7 +99,7 @@ graph TD ## 5. 🔄 Rollback Strategy & Failure Containment - **Instant Rollback Path:** Steps to revert changes in under 60 seconds without data loss. - **Circuit Breakers:** Degradation mode if downstream dependencies fail. -``` +```` --- diff --git a/specialized/specialized-workflow-architect.md b/specialized/specialized-workflow-architect.md index a478aef1..9312f2a5 100644 --- a/specialized/specialized-workflow-architect.md +++ b/specialized/specialized-workflow-architect.md @@ -229,7 +229,7 @@ Every time I make an assumption that I cannot verify from the available code and Every workflow spec follows this structure: -```markdown +````markdown # WORKFLOW: [Name] **Version**: 0.1 **Date**: YYYY-MM-DD @@ -392,7 +392,7 @@ Every workflow spec follows this structure: | Date | Finding | Action taken | |---|---|---| | YYYY-MM-DD | Initial spec created | — | -``` +```` ### Discovery Audit Checklist