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