diff --git a/.claude/agents/tech-writer.md b/.claude/agents/tech-writer.md index abb873d7f0..49c38a4147 100644 --- a/.claude/agents/tech-writer.md +++ b/.claude/agents/tech-writer.md @@ -1,6 +1,6 @@ --- name: tech-writer -description: "Use this agent when a writer needs autonomous documentation work completed end-to-end: drafting new documentation from a specification, reviewing and fixing Vale issues in a file, or editing existing content for style and clarity. Give it a task and it will complete it.\n\nExamples:\n\n- Example 1:\n user: \"Write a getting started guide for PingCastle based on this spec\"\n assistant: \"I'll launch the tech-writer agent to draft this documentation.\"\n A well-defined writing task — the agent can draft autonomously from a spec.\n\n- Example 2:\n user: \"Fix all the Vale errors in docs/accessanalyzer/12.0/install.md\"\n assistant: \"I'll have the tech-writer agent review and fix the Vale issues.\"\n A concrete, bounded task the agent can complete end-to-end.\n\n- Example 3:\n user: \"Edit this procedure for clarity and Netwrix style\"\n assistant: \"I'll launch the tech-writer agent to review and improve this content.\"\n The agent can apply style and clarity improvements autonomously." +description: "Autonomous end-to-end documentation agent: drafting from specs, editing for style and clarity, or incorporating external documents. Dispatch with opus for drafting; sonnet for style edits and document incorporation." model: opus color: purple memory: project @@ -10,7 +10,7 @@ You are an expert technical writer for Netwrix, a cybersecurity company that bui Your background: you've written production code at scale, shipped security products to enterprise customers, and owned documentation end-to-end at a fast-moving company. You understand how software is actually built and what customers actually need to know. You don't just document features — you explain them in a way that makes readers feel capable and confident. -You write clearly, conversationally, concisely, and consistently. Every concept you introduce comes with an example. You anticipate the questions readers will have and answer them before they're asked. You write for newer users without condescending to experienced ones. +You write clearly, conversationally, concisely, and consistently. Every concept you introduce comes with an example. You anticipate the questions readers will have and answer them before they're asked. You provide enough context for newer users to follow along without over-explaining things experienced users already know. **Always read `docs/CLAUDE.md` before starting any task.** It contains the Netwrix conventions, Vale rules, file structure, and content patterns you must follow. @@ -18,7 +18,11 @@ You write clearly, conversationally, concisely, and consistently. Every concept You are an autonomous agent. When given a task, you complete it end-to-end using the tools available to you. You don't ask unnecessary questions — you read the relevant files, understand the context, do the work, and report what you did. -If something would fundamentally change your approach, ask once, concisely. Otherwise, make a reasonable judgment and proceed. +If the task is ambiguous, ask one clarifying question before proceeding. Otherwise, make a reasonable judgment and proceed. + +Before starting work, create a todo for each step of your task using the TaskCreate tool. Mark each task complete as you finish it. This gives the user visibility into your progress on long-running tasks. + +After editing docs files, hooks will suggest running Vale and Dale. Linting is handled separately by hooks and CI. ## Task Types @@ -30,18 +34,7 @@ If something would fundamentally change your approach, ask once, concisely. Othe 4. Draft the content following Netwrix structure: overview → prerequisites → procedures 5. Include examples for every concept introduced 6. Anticipate reader questions and answer them inline -7. Run Vale on the drafted file and fix all reported issues -8. Run the dale skill on the drafted file and fix any warnings -9. Report what you wrote and the key structural decisions you made - -### Review and fix Vale issues - -1. Read `docs/CLAUDE.md` for Vale guidance, especially the three rules requiring extra care -2. Run `vale ` and capture all errors -3. Fix each error — read the surrounding context before substituting; never blindly replace -4. Re-run Vale until zero errors remain -5. Run the dale skill on the file and fix any warnings -6. Report the changes made, grouped by rule +7. Reread the drafted file. Fix any passive voice, hedging, future tense describing software behavior, wordiness, or idioms. ### Edit for style and clarity @@ -49,11 +42,27 @@ If something would fundamentally change your approach, ask once, concisely. Othe 2. Read the full document before making any changes 3. Identify issues: passive voice, weak link text, missing examples, inconsistent terminology, overly long sentences 4. Edit with a light hand — preserve the author's meaning; improve the expression -5. Run Vale after editing and fix any new violations introduced -6. Run the dale skill on the file and fix any warnings -7. Report the substantive changes made and why +5. Reread the file. Fix any passive voice, hedging, future tense describing software behavior, wordiness, or idioms. + +### Incorporate external documents -Always run Vale and the dale skill before reporting a task complete. +When given an external document (e.g., `.docx`, `.pdf`, or pasted content) to merge into an existing markdown file: + +1. Read `docs/CLAUDE.md` for conventions +2. Read the external document to understand its content and structure +3. Read the target markdown file in full +4. Identify where the new content fits — match the existing document's structure and heading hierarchy +5. Merge the content, adapting it to Netwrix style: active voice, present tense, imperative procedures, examples for every concept +6. Remove any content from the external document that duplicates what's already in the target file +7. Reread the merged file. Fix any passive voice, hedging, future tense describing software behavior, wordiness, or idioms. + +### Multi-file tasks + +When a task covers multiple files (e.g., "fix Vale issues in `docs/accessanalyzer/12.0/`"): + +1. List all matching files first using Glob +2. Create one todo per file +3. Process files one at a time — complete all steps for each file before moving to the next ## Output Style @@ -78,3 +87,53 @@ The difference: - **Active, not passive.** "The monitoring plan collects" vs. "data will be transmitted." - **Procedural steps are instructions, not descriptions.** "Go to Settings" vs. "navigating to the appropriate settings." - **No throat-clearing.** Never start with "It should be noted that" or "Please be aware that." + +## Style Reference + +Vale and Dale run via hooks after you edit files and automatically on PRs. The self-review step in each task type covers the same issues Dale checks (passive voice, wordiness, idioms, hedging, future tense). The rules below cover what linters don't catch. Apply these while writing. + +### Grammar + +- **Contractions**: Use common contractions (don't, can't, you'll). Avoid unusual ones (should've, could've). +- **Anthropomorphism**: Don't attribute human traits to software. "The system displays" not "the system sees." +- **Parallel structure**: Items in a list or series use the same grammatical form. +- **Nominalizations**: Use verbs, not nouns derived from verbs. "Configure" not "perform the configuration of." +- **One idea per sentence**: Break compound sentences that cover multiple concepts. +- **Articles**: Don't omit articles (a, an, the) for brevity. +- **That/which**: "That" for restrictive clauses (no comma). "Which" for nonrestrictive (with comma). +- **Who/whom**: "Who" for subjects, "whom" for objects. +- **Since/because**: "Since" for time, "because" for causation. +- **While/although**: "While" for time, "although" for contrast. +- **Whether/if**: "Whether" for alternatives, "if" for conditions. +- **Fewer/less**: "Fewer" for countable, "less" for uncountable. +- **Collective nouns**: Singular in American English. "The team configures" not "the team configure." +- **Gendered pronouns**: Avoid. Repeat the noun instead of using he/she or singular they. + +### Formatting + +- **Headings**: Sentence case. Infinitive for tasks ("Install the agent"), gerund for concepts ("Reviewing audit logs"). +- **Bold**: UI elements, buttons, menu items. +- **Code formatting**: Commands, file paths, technical values. +- **No italics**. +- **Oxford comma**: Required. +- **Em dashes**: No spaces (word—word). +- **Hyphens**: Compound modifiers before nouns ("real-time monitoring" but "runs in real time"). +- **Numbers**: Spell out 0–9, numerals for 10+. Numerals with units (5 GB). Commas in thousands (1,500). +- **Dates**: Month Day, Year (January 15, 2025). +- **Time**: 12-hour clock with AM/PM. + +### Terminology + +- **Inclusive terms**: allowlist/denylist, primary/replica — not whitelist/blacklist, master/slave. +- **Version comparisons**: "or later" / "or earlier" — not "or higher" / "or newer." +- **No time-relative qualifiers**: No "currently", "as of this writing", or pre-announcing future features. + +### Structure + +- Concepts before procedures: overview → prerequisites → steps. +- Examples immediately after the concept they illustrate. +- Common tasks before advanced topics. +- Cross-references at the end of sections. +- Alt text on every image. + +For the full style guide with detailed examples, see `netwrix_style_guide.md` in the project root. diff --git a/.github/workflows/md-extension-autofix.yml b/.github/workflows/md-extension-autofix.yml index 269c5aec91..9fec086b82 100644 --- a/.github/workflows/md-extension-autofix.yml +++ b/.github/workflows/md-extension-autofix.yml @@ -25,7 +25,7 @@ jobs: --jq '{message: .commit.message}') MESSAGE=$(echo "$COMMIT" | jq -r '.message') echo "Latest commit message: $MESSAGE" - if echo "$MESSAGE" | grep -qE '^fix\(docs\): add missing \.md extension'; then + if echo "$MESSAGE" | grep -qE '^fix\(docs\): add missing \.md extension and inject frontmatter'; then echo "Skipping: commit is from md-extension-autofix workflow" echo "skip=true" >> "$GITHUB_OUTPUT" else @@ -36,10 +36,9 @@ jobs: if: steps.bot-check.outputs.skip != 'true' uses: actions/checkout@v4 with: - ref: ${{ github.event.pull_request.base.ref }} + ref: ${{ github.event.pull_request.head.ref }} fetch-depth: 0 - - name: Configure git identity if: steps.bot-check.outputs.skip != 'true' run: | @@ -71,11 +70,13 @@ jobs: echo "$SUMMARY" > /tmp/md-extension-summary.json RENAMED_COUNT=$(echo "$SUMMARY" | jq '.renamed | length') SKIPPED_COUNT=$(echo "$SUMMARY" | jq '.skipped | length') + FRONTMATTER_COUNT=$(echo "$SUMMARY" | jq '.frontmatter_injected | length') echo "renamed=$RENAMED_COUNT" >> "$GITHUB_OUTPUT" echo "skipped=$SKIPPED_COUNT" >> "$GITHUB_OUTPUT" - echo "Renamed: $RENAMED_COUNT, Skipped: $SKIPPED_COUNT" + echo "frontmatter=$FRONTMATTER_COUNT" >> "$GITHUB_OUTPUT" + echo "Renamed: $RENAMED_COUNT, Skipped: $SKIPPED_COUNT, Frontmatter injected: $FRONTMATTER_COUNT" - - name: Commit renamed files + - name: Commit fixes id: commit if: steps.autofix.outputs.renamed > 0 run: | @@ -83,7 +84,7 @@ jobs: echo "committed=false" >> "$GITHUB_OUTPUT" else git add -A docs/ - git commit -m "fix(docs): add missing .md extension to renamed files" + git commit -m "fix(docs): add missing .md extension and inject frontmatter" echo "committed=true" >> "$GITHUB_OUTPUT" fi @@ -106,9 +107,11 @@ jobs: RENAMED_COUNT=0 SKIPPED_COUNT=0 + FRONTMATTER_COUNT=0 if [ -f /tmp/md-extension-summary.json ]; then RENAMED_COUNT=$(jq '.renamed | length' /tmp/md-extension-summary.json) SKIPPED_COUNT=$(jq '.skipped | length' /tmp/md-extension-summary.json) + FRONTMATTER_COUNT=$(jq '.frontmatter_injected | length' /tmp/md-extension-summary.json) fi # Nothing to report — exit silently @@ -129,8 +132,19 @@ jobs: echo "Links to these files in other pages have been updated. Please review the changes in the commit above." fi + if [ "$FRONTMATTER_COUNT" -gt 0 ]; then + echo "" + echo "**Frontmatter injected**" + echo "" + echo "The following files were missing frontmatter. \`title\`, \`description\`, and \`sidebar_position\` were derived automatically and may need manual review:" + echo "" + echo "| File |" + echo "|---|" + jq -r '.frontmatter_injected[] | "| `\(.)` |"' /tmp/md-extension-summary.json + fi + if [ "$SKIPPED_COUNT" -gt 0 ]; then - if [ "$RENAMED_COUNT" -gt 0 ]; then echo ""; fi + echo "" echo "**Action needed: Possible missing \`.md\` extension**" echo "" echo "The following files were added to a docs directory without a \`.md\` extension, but couldn't be auto-renamed:" diff --git a/scripts/md-extension-autofix.sh b/scripts/md-extension-autofix.sh index faa6bd1b89..94b4d193d5 100644 --- a/scripts/md-extension-autofix.sh +++ b/scripts/md-extension-autofix.sh @@ -32,15 +32,54 @@ is_ignored_extension() { is_markdown_content() { local file="$1" - local has_frontmatter=0 - local has_heading=0 - if head -10 "$file" | grep -qE '^---'; then - has_frontmatter=1 - fi - if grep -qE '^#{1,6} ' "$file"; then - has_heading=1 - fi - [ "$has_frontmatter" -eq 1 ] && [ "$has_heading" -eq 1 ] + grep -qE '^#{1,6} ' "$file" +} + +has_frontmatter() { + local file="$1" + head -1 "$file" | grep -qE '^---' +} + +extract_h1() { + local file="$1" + grep -m 1 -E '^# ' "$file" | sed 's/^# //' +} + +calculate_sidebar_position() { + local file="$1" + local dir + dir=$(dirname "$file") + local basename + basename=$(basename "$file") + local position=1 + local count=0 + while IFS= read -r sibling; do + count=$((count + 1)) + if [ "$(basename "$sibling")" = "$basename" ]; then + position=$count + fi + done < <(find "$dir" -maxdepth 1 -name '*.md' | sort) + echo $((position * 10)) +} + +inject_frontmatter() { + local file="$1" + local title + title=$(extract_h1 "$file") + local position + position=$(calculate_sidebar_position "$file") + local tmp + tmp=$(mktemp) + { + echo "---" + echo "title: \"$title\"" + echo "description: \"$title\"" + echo "sidebar_position: $position" + echo "---" + echo "" + cat "$file" + } > "$tmp" + mv "$tmp" "$file" } rewrite_links_in_docs() { @@ -66,7 +105,7 @@ esac CHANGED_FILES_LIST="${1:?Usage: md-extension-autofix.sh }" if [ ! -f "$CHANGED_FILES_LIST" ]; then - echo '{"renamed": [], "skipped": []}' + echo '{"renamed": [], "skipped": [], "frontmatter_injected": []}' exit 0 fi @@ -74,6 +113,7 @@ RENAMED_FROM=() RENAMED_TO=() SKIPPED_FILES=() SKIP_REASONS=() +FRONTMATTER_INJECTED=() while IFS= read -r file; do # Only process files inside docs/ @@ -82,8 +122,15 @@ while IFS= read -r file; do # Skip deleted files [ -f "$file" ] || continue - # Skip files that already have an extension - has_extension "$file" && continue + # Skip files that already have an extension — but still inject frontmatter into .md docs files missing it + if has_extension "$file"; then + if [[ "$file" == *.md ]] && [[ "$file" != docs/kb/* ]] && ! has_frontmatter "$file" && is_markdown_content "$file"; then + inject_frontmatter "$file" + git add "$file" + FRONTMATTER_INJECTED+=("$file") + fi + continue + fi # Skip files with a known non-markdown extension is_ignored_extension "$file" && continue @@ -108,12 +155,19 @@ while IFS= read -r file; do # Use mv + git add instead of git mv so it works for both tracked and untracked files mv "$file" "$new_file" git add "$new_file" - git rm --cached "$file" 2>/dev/null || true + git rm -q --cached "$file" 2>/dev/null || true rewrite_links_in_docs "$(basename "$file")" "$(basename "$new_file")" RENAMED_FROM+=("$file") RENAMED_TO+=("$new_file") + # Inject frontmatter into renamed docs files (not KB — handled by derek skill) + if [[ "$new_file" != docs/kb/* ]] && ! has_frontmatter "$new_file"; then + inject_frontmatter "$new_file" + git add "$new_file" + FRONTMATTER_INJECTED+=("$new_file") + fi + done < "$CHANGED_FILES_LIST" # Output JSON summary @@ -131,4 +185,11 @@ for i in "${!SKIPPED_FILES[@]}"; do done SKIPPED_JSON+="]" -echo "{\"renamed\": ${RENAMED_JSON}, \"skipped\": ${SKIPPED_JSON}}" +FRONTMATTER_JSON="[" +for i in "${!FRONTMATTER_INJECTED[@]}"; do + [ "$i" -gt 0 ] && FRONTMATTER_JSON+="," + FRONTMATTER_JSON+="\"${FRONTMATTER_INJECTED[$i]}\"" +done +FRONTMATTER_JSON+="]" + +echo "{\"renamed\": ${RENAMED_JSON}, \"skipped\": ${SKIPPED_JSON}, \"frontmatter_injected\": ${FRONTMATTER_JSON}}" diff --git a/scripts/test-md-extension-autofix.sh b/scripts/test-md-extension-autofix.sh index 29d6bd3eef..f9b2ae518b 100644 --- a/scripts/test-md-extension-autofix.sh +++ b/scripts/test-md-extension-autofix.sh @@ -78,14 +78,14 @@ EOF is_markdown_content "$TMPDIR_TEST/good.md" && R=$? || R=$? assert_true "markdown: has frontmatter and heading" "$R" -# File with only heading, no frontmatter → not markdown +# File with only heading, no frontmatter → still markdown (frontmatter will be injected) cat > "$TMPDIR_TEST/no-frontmatter" <<'EOF' # My Heading Some content here. EOF is_markdown_content "$TMPDIR_TEST/no-frontmatter" && R=$? || R=$? -assert_false "not markdown: heading only, no frontmatter" "$R" +assert_true "markdown: heading only, no frontmatter" "$R" # File with only frontmatter, no heading → not markdown cat > "$TMPDIR_TEST/no-heading" <<'EOF' @@ -148,6 +148,94 @@ assert_true "link rewrite: relative path link updated" "$R" rm -rf "$TMPDIR_LINKS" +# ---- has_frontmatter ---- + +TMPDIR_FM=$(mktemp -d) + +cat > "$TMPDIR_FM/with-frontmatter.md" <<'EOF' +--- +title: Test +--- + +# Heading +EOF + +cat > "$TMPDIR_FM/no-frontmatter.md" <<'EOF' +# Heading + +No frontmatter here. +EOF + +has_frontmatter "$TMPDIR_FM/with-frontmatter.md" && R=$? || R=$? +assert_true "has_frontmatter: file starts with ---" "$R" + +has_frontmatter "$TMPDIR_FM/no-frontmatter.md" && R=$? || R=$? +assert_false "has_frontmatter: file does not start with ---" "$R" + +# ---- extract_h1 ---- + +cat > "$TMPDIR_FM/heading.md" <<'EOF' +--- +title: Test +--- + +# My Article Title +EOF + +RESULT=$(extract_h1 "$TMPDIR_FM/heading.md") +[ "$RESULT" = "My Article Title" ] && R=0 || R=1 +assert_true "extract_h1: returns heading text without # prefix" "$R" + +# ---- calculate_sidebar_position ---- + +mkdir -p "$TMPDIR_FM/siblings" +touch "$TMPDIR_FM/siblings/apple.md" +touch "$TMPDIR_FM/siblings/banana.md" +touch "$TMPDIR_FM/siblings/cherry.md" +touch "$TMPDIR_FM/siblings/date.md" +touch "$TMPDIR_FM/siblings/elderberry.md" + +RESULT=$(calculate_sidebar_position "$TMPDIR_FM/siblings/cherry.md") +[ "$RESULT" = "30" ] && R=0 || R=1 +assert_true "calculate_sidebar_position: 3rd of 5 → 30" "$R" + +RESULT=$(calculate_sidebar_position "$TMPDIR_FM/siblings/apple.md") +[ "$RESULT" = "10" ] && R=0 || R=1 +assert_true "calculate_sidebar_position: 1st of 5 → 10" "$R" + +# ---- inject_frontmatter ---- + +mkdir -p "$TMPDIR_FM/article-dir" +touch "$TMPDIR_FM/article-dir/another.md" + +cat > "$TMPDIR_FM/article-dir/my-article.md" <<'EOF' +# My Article + +Some content here. +EOF + +inject_frontmatter "$TMPDIR_FM/article-dir/my-article.md" + +head -1 "$TMPDIR_FM/article-dir/my-article.md" | grep -q '^---' && R=0 || R=1 +assert_true "inject_frontmatter: file now starts with ---" "$R" + +grep -q 'title: "My Article"' "$TMPDIR_FM/article-dir/my-article.md" && R=0 || R=1 +assert_true "inject_frontmatter: title derived from h1" "$R" + +grep -q 'description: "My Article"' "$TMPDIR_FM/article-dir/my-article.md" && R=0 || R=1 +assert_true "inject_frontmatter: description matches title" "$R" + +grep -q 'sidebar_position:' "$TMPDIR_FM/article-dir/my-article.md" && R=0 || R=1 +assert_true "inject_frontmatter: sidebar_position present" "$R" + +grep -q '^# My Article' "$TMPDIR_FM/article-dir/my-article.md" && R=0 || R=1 +assert_true "inject_frontmatter: original h1 preserved" "$R" + +grep -q 'Some content here.' "$TMPDIR_FM/article-dir/my-article.md" && R=0 || R=1 +assert_true "inject_frontmatter: original body content preserved" "$R" + +rm -rf "$TMPDIR_FM" + echo "" echo "Results: $PASS passed, $FAIL failed" if [ "$FAIL" -gt 0 ]; then