aboutsummaryrefslogtreecommitdiff
Commit message (Collapse)AuthorAgeFilesLines
* chore(ai): session record for todo-cleanup --archive-done + clean-todo workCraig Jennings2026-05-111-0/+75
| | | | todo-cleanup.el gained --archive-done (level-2 DONE/CANCELLED subtrees move to "Resolved"), with a 13-test ERT suite wired into make test. The wrap-up flow now runs both the hygiene pass and --archive-done, and clean-todo is the on-demand entry point.
* docs(workflows): add clean-todo workflowCraig Jennings2026-05-112-0/+60
| | | | clean-todo is the manual entry point for tidying todo.org: it runs the hygiene pass, then --archive-done (relocate completed level-2 subtrees into "Resolved"), then summarizes what changed and leaves the diff uncommitted for review. The wrap-up flow already does both passes at session end; clean-todo runs them on demand. It's listed in INDEX.org under the usual trigger phrases.
* docs(workflows): run todo-cleanup --archive-done in wrap-upCraig Jennings2026-05-111-3/+21
| | | | The wrap-up flow already runs the hygiene pass on todo.org; it now also runs --archive-done, which relocates completed level-2 subtrees from "Open Work" to "Resolved". Both passes are idempotent and skip cleanly when the file lacks the named sections, and any moves land in the wrap-up commit's diff for review before push.
* docs(todo): mark --archive-done task doneCraig Jennings2026-05-111-11/+13
|
* feat(todo-cleanup): add --archive-done mode with ERT test suiteCraig Jennings2026-05-114-30/+575
| | | | | | | | --archive-done moves every level-2 subtree whose TODO state is DONE or CANCELLED out of the "Open Work" section into the "Resolved" section of the same org file, subtree intact. Sections match on a unique level-1 heading containing "Open Work" (case-insensitive) and one containing "Resolved"; a missing or ambiguous section skips the file with a message rather than crashing. Only direct level-2 children move. A DONE entry nested under an open parent stays put. Opt-in, never run by default, doesn't also run the hygiene passes; --check previews without writing. The CLI dispatch moved into tc-main behind a guard so the new ERT suite can require the file without firing it. Hygiene mode is unchanged. 13 ERT cases (the repo's first elisp tests) cover the move and the stay-put cases, EOF with no final newline, missing or ambiguous sections, lowercase headings, idempotency, and --check. tests/fixtures/todo-sample.org is the synthetic sample, and the Makefile test target now runs the ERT suites alongside pytest.
* docs(workflows): require every worktree leftover to be resolved at wrapCraig Jennings2026-05-111-4/+35
| | | | Replace the old "intentional carryover" default in wrap-it-up.org. End every session with an empty `git status`: classify each leftover as a runtime artifact, a forgotten change, or pre-existing dirt, apply a concrete resolution unless the user explicitly defers, and log any deferral in the valediction so the next session knows it was a choice, not a miss.
* docs(todo): add --archive-done task for todo-cleanup.elCraig Jennings2026-05-111-0/+11
|
* chore(ai): sync template updates from claude-templatesCraig Jennings2026-05-115-20/+85
| | | | Pull in the latest maildir-flag-manager.py and cross-agent-comms doc updates from the claude-templates source.
* docs(todo): add make-audit and claude-templates-fold tasksCraig Jennings2026-05-111-0/+39
| | | | I added two [#B] entries. The first folds the standalone claude-templates repo into rulesets/claude-templates/ via subtree merge, bridging the path change with a transitional symlink while every project picks up the updated startup.org. The second adds a make audit target that diffs each .ai/-using project against the canonical template source. Both are on hold for now — the entries just record the plan.
* docs(todo): nest entries under a "Rulesets Open Work" headingCraig Jennings2026-05-111-194/+199
| | | | Add a single top-level "Rulesets Open Work" heading and demote every entry one level so the file has one root section.
* docs(commits): add Step 0 pre-flight reconcile and pre-push checkCraig Jennings2026-05-101-1/+51
| | | | | | | | The publish flow had no fetch step before commit, PR creation, or push. Long sessions or multi-machine work could land local commits on a stale base, producing non-fast-forward push failures that you have to unwind under publish-step pressure. Step 0 fetches all remotes and checks the current branch against its upstream before Step 1's code review. If the branch is behind, the rule branches on tree state and divergence shape: clean fast-forward, surface dirty-tree behind, or surface a true divergence and ask before rebasing or merging. The Step 0 wording covers the new-branch case (no upstream → skip the divergence check, the first push sets it). The Pre-push reconcile bullet in Merge Strategy handles the smaller window between Step 0 and the actual push. Reviewing and drafting can take several minutes; another machine or teammate can push during that window. One more fetch immediately before push is cheaper than recovering after a failed push.
* fix(gmail): Improve safe_filename to handle .. prefixesCraig Jennings2026-05-081-2/+11
| | | | | | Strip leading ".." sequences instead of stripping all leading dots, so dotfiles like ".gitignore" are preserved while still preventing directory traversal via "../foo" style names. ```
* docs(workflows): Update DeepSat repo path in daily-prepCraig Jennings2026-05-081-1/+1
|
* feat: Add cmail IMAP action script and test suiteCraig Jennings2026-05-084-0/+1786
| | | | | | | | Add cmail-action.py for IMAP triage operations against Proton Mail Bridge (list-unread, read, mark-read, star, unstar, trash, send, folders) mirroring the Gmail MCP workflow. Also add comprehensive tests for cmail-action, gmail-fetch-attachments, and maildir-flag-manager scripts.
* feat(scripts): add readability tool + pre-warm textstat in depsCraig Jennings2026-05-082-0/+116
| | | | | | | | Adds scripts/readability — a Python tool that prints standard readability metrics (Flesch Reading Ease, Flesch-Kincaid Grade, Gunning Fog, SMOG, Coleman-Liau, ARI, Dale-Chall, Linsear-Write) for one input file or as a side-by-side comparison of two. Self-contained via PEP 723 inline metadata: textstat is declared as the script's only dependency, and the `#!/usr/bin/env -S uv run --quiet --script` shebang lets uv resolve it on each invocation. The Makefile `deps` target now also pre-warms textstat in uv's cache so the first interactive run is fast.
* feat(claude-rules): add interaction.md — no popup menus for choicesCraig Jennings2026-05-081-0/+31
| | | | | | Codify the rule that AskUserQuestion's popup menu obscures the chat text the user needs to read to make the choice. Choice prompts go inline as numbered options instead, with a "pick a number" prompt at the end. Applies to all three approval gates in commits.md (commit message, PR description, PR review reply).
* chore(ai): correct stale ~/projects/work/ path references in workflows and ↵Craig Jennings2026-05-084-5/+5
| | | | scripts
* feat(wrap-it-up): add Linear Dev-Review sweep stepCraig Jennings2026-05-081-2/+23
| | | | Sweep Dev-Review tickets assigned to Craig before the wrap-up commit. Any whose linked PR has merged gets proposed for Done (chores, refactors, test backfills, dead-code removal) or PM Acceptance (real fixes or features users can verify). Tickets stuck in Dev Review after the PR merges hide real in-progress work. The step is idempotent and skips when Linear is not in use.
* feat(make): add bootstrap target for fresh-machine setupCraig Jennings2026-05-081-0/+9
| | | | Bootstrap chains install, install-hooks, and install-mcp into one command. The three targets stay split so routine re-symlinking stays cheap (no GPG pinentry, no network), but bootstrap gives the fresh-install case one entry point. The gap surfaced on a fresh machine where doctor flagged 4 hook warnings and 8 MCP failures.
* docs(todo): add follow-ups for category-3 rules and language-rule auditCraig Jennings2026-05-071-0/+19
| | | | | | | Two open decisions from the 2026-05-07 rulesets centralization pass: - Category-3 rule copies in the deepsat tree (`coding-rulesets/` and `orchestration_dashboard_mvp/`) — read each and decide between leave, sync, or symlink. - Language-specific rule files (python-testing, typescript-testing, elisp-testing, elisp) duplicated across multiple project mirrors — audit and possibly canonicalize.
* fix(commits): anchor .ai/ detection to repo root with :/ pathspecCraig Jennings2026-05-071-2/+4
| | | | | | | | The detection command for personal vs. general voice mode used `git ls-files .ai/`, which returns no matches when run from a subdirectory of the repo, even when `.ai/` is tracked at the root. That silently misclassified projects as personal-voice when they should have been general-voice. Switching to `git ls-files :/.ai/` anchors the search to the repo root via the `:/` pathspec, so the command works correctly from any cwd. I hit this myself today: ran the check from `claude-rules/` inside the rulesets repo, got an empty result, and applied `/voice personal` to a commit that should have used `/voice` (general mode).
* docs(commits): add bundled-review shape, voice mode gating, drop humanizerCraig Jennings2026-05-071-16/+101
| | | | | | | | I rewrote the PR review subflow into three explicit shapes. Shape 1 is a single review that bundles the verdict, the summary body, and zero or more inline pins into one `gh api .../reviews` call. Shape 2 is an issue-thread comment with no verdict. Shape 3 is a reply on an existing inline thread. The single-review path replaces the prior pattern where a Request-Changes verdict with line-specific findings needed separate `gh pr review` + `gh pr comment` calls. That fragmented the Slack notification and the review history. I migrated all `humanizer` references to `/voice personal`. The voice skill replaced humanizer, so the old name was dead. I dropped the two lineage mentions of "humanizer's signs of AI writing" since they pointed at a skill that no longer exists. I added a Voice mode and approval gate preamble at the top of Step 2. The mode is decided by whether `.ai/` is tracked in the repo. Gitignored or absent means personal-voice with the full approval gate. Tracked means general-voice and the gate is skipped, since the personal-only patterns (first-person rewrite, contractions, semicolon swap) don't fit a shared rules file. I also updated the Single-skill gate wrap-up paragraph at the end of Step 2 to reference both modes.
* feat: voice skill, make doctor, MCP token bundlingCraig Jennings2026-05-072-151/+310
| | | | | | | | Voice skill consolidates the prose-quality passes (humanizer plus universal good-writing rules plus personal-style) into one /voice invocation with two modes. General mode for arbitrary writing. Personal mode for commit messages, PR descriptions, and PR review comments. The standalone humanizer skill is retired. make doctor verifies ~/.claude/ live state matches the repo and settings.json. Eight checks covering skills, rules, hooks, settings.json hook references, plugins, MCP server registrations, and dangling symlinks. The MCP install pipeline now bundles Google Docs OAuth tokens alongside the GCP keys, so a fresh machine boots fully connected after make install-mcp without requiring a manual OAuth dance per profile.
* chore(skills): remove humanizer (superseded by voice)Craig Jennings2026-05-074-476/+212
| | | | | | | | I deleted humanizer/SKILL.md now that all three callers (commits.md, respond-to-cj-comments.md, start-work.md) invoke /voice instead. The 25 humanizer patterns live on as patterns 1-25 in voice/SKILL.md. Same source (Wikipedia's Signs of AI writing), same prose, same examples — just renumbered alongside the universal good-writing additions and the personal-only patterns. I also updated .ai/notes.org and .ai/workflows/wrap-it-up.org to reference /voice personal instead of the old humanizer + manual-passes flow. The wrap-it-up change landed upstream in claude-templates first so it survives the next startup rsync. todo.org gets the matching update: the voice TODO is marked DONE with a "Built and shipped" timestamp, the publish-mode terminology is renamed to personal-mode throughout, the V1 scope checklist is ticked, the open questions are resolved with the answers we landed on during implementation, and the migration section records the delete-not-alias decision.
* chore: migrate humanizer callers to /voice personalCraig Jennings2026-05-073-51/+30
| | | | | | | | | | I switched the three publish subflows in commits.md (commit messages, PR descriptions, PR review comments) from "run humanizer; apply five personal-style passes in order" to a single "run /voice personal" invocation. The new skill walks 39 patterns in one editorial review and absorbs the five passes wholesale, plus four more personal-style additions (felt-experience cut, fragment-in-prose rewrite, terse cut, public-artifact scope flag) and six universal good-writing patterns. The numbered steps in each subflow collapse from 5 to 4 (commits) and 9 to 8 (PRs) since the dedicated personal-style step folds into the voice invocation. The Multi-pass gate paragraph becomes a Single-skill gate. The mid-flow "all the passes" prompt now means re-run the full 39-pattern walk in personal mode rather than reapplying six discrete steps. I also updated respond-to-cj-comments.md to invoke /voice personal for public writing and /voice general for the lighter pass on internal notes when wanted, and updated start-work.md's Phase 7 summary to match. The humanizer skill itself stays in place for now. The next commit removes it.
* feat(skills): add voice skill (humanizer + universal + personal passes)Craig Jennings2026-05-071-0/+635
| | | | | | | | I built voice as a single skill that walks 39 numbered prose-editing patterns. The first 25 patterns come straight from the existing humanizer skill (Wikipedia's Signs of AI Writing). Patterns 26-31 add universal good-writing rules from Strunk & White, Orwell, the Plain English Campaign, and Garner — long-word → short-word, active-over-passive, comma splices, cliché flag, jargon-fragment-in-prose rewrite, and corporate-speak nominalizations. Patterns 32-39 are tagged "personal only" and cover first-person rewrite, semicolon swaps, contractions, sentence-split on conjunctions, felt-experience cut, sentence-fragment-in-prose rewrite, terse cut for rhetorical padding, and a public-artifact scope flag. Two modes determine which patterns get walked. General mode (default) walks 1-31 and is the right fit for research notes, philosophy and history essays, emails, README prose, journal entries, anything that isn't a commit, PR, or PR review comment. Personal mode walks all 39 patterns and is invoked explicitly by the publish flow in commits.md (and similar callers) so first-person and contraction enforcement don't leak into academic or literary writing where they don't belong. The follow-up commits migrate callers to /voice and remove the standalone humanizer skill.
* feat(make): add doctor target for ~/.claude drift detectionCraig Jennings2026-05-073-1/+264
| | | | | | | | | | | | =make doctor= scans =~/.claude/= and reports drift against the repo + settings.json. Read-only diagnostic. Eight checks cover skills, rules, default hooks, claude config, settings.json hook references, enabledPlugins, MCP server registrations, and dangling symlinks. Each line prints =ok= / =WARN= / =FAIL= with a final summary. Exit 1 on any FAIL. A sweep last night found =~/.claude/hooks/= didn't exist on this machine even though =settings.json= referenced a PreCompact hook there. Compaction would have silently failed to invoke it. doctor catches that kind of drift in one command instead of relying on a manual look. The MCP drift check reads =~/.claude.json= directly rather than parsing =claude mcp list=. The CLI has no JSON output and runs a per-server health probe (~10s). The JSON file is the user-scope source of truth for registrations and parses in well under a second. I verified by injecting four drift scenarios — removed hook symlink, removed skill symlink, moved-aside plugin data dir, unregistered MCP server. Each produced the expected =FAIL= line and exit 1. After restoring state, doctor came back clean (33 ok). Bundling four other improvement TODOs from the same sweep — =mcp/README.org=, =make uninstall-mcp= and =mcp/install.py --check=, a README.org section for the MCP install pipeline, and a token-rotation helper for =@a-bonus/google-docs-mcp= OAuth refresh. Plus a stale-bullet note on the existing =make remove= TODO (the bridge symlink it references was removed earlier).
* chore(mcp): mark install.py executableCraig Jennings2026-05-061-0/+0
| | | | The shebang was already there but the file mode was 644. The Makefile invokes via "python3 mcp/install.py" so it worked anyway, but the mode now matches the shebang.
* chore(ai): sync template updates from claude-templatesCraig Jennings2026-05-065-5/+12
| | | | | | I added a "Shell aliases (=ls= → =exa=)" note to protocols.org so future sessions know to use \ls when capturing ls output programmatically. exa prints nothing to non-TTY pipes, so the symptom looks like an empty directory. I hit this earlier in the session when a sweep came back blank for a directory I knew was populated. I also fixed three stale ~/projects/career/ examples in cross-agent-comms/ docs that didn't get updated when career was renamed to work, and the daily-prep.org path leak from last session (~/code/deepsat/... → ~/projects/work/deepsat/code/...). The authoritative edits live in claude-templates. These rulesets snapshots landed via the standard rsync from upstream.
* chore(make): remove unused claude-rules bridge symlinkCraig Jennings2026-05-061-18/+0
| | | | | | The bridge at ~/.claude/skills/claude-rules existed so a SKILL.md could resolve a relative path like ../claude-rules/testing.md from the install layout. No SKILL.md actually uses that pattern. Every reference I grepped — across debug, add-tests, and pairwise-tests — names the rule file by bare filename in prose, which doesn't go through any link. The symlink was defensive scaffolding for a use case that didn't land. The four rule files keep loading via ~/.claude/rules/ unchanged. Claude Code's skill harness was silently ignoring the bridge anyway because the target directory has no SKILL.md, so no behavior moves except that make install stops creating the dead entry. If a future SKILL.md wants deep-linking, the bridge can come back deliberately.
* feat(mcp): add user-scope MCP install pipelineCraig Jennings2026-05-065-1/+230
| | | | | | | | | | I needed a single source of truth for MCP server registration so a fresh machine boots with the full set instead of being rebuilt by hand. install.py decrypts mcp/secrets.env.gpg, expands ${VAR} placeholders in mcp/servers.json, and runs claude mcp add --scope user for anything not already registered. Idempotent. The encrypted bundle carries six values: the Google client id and secret, the Figma API key, the GCP OAuth keys JSON (base64), and the two @a-bonus/google-docs-mcp token caches (personal and work, base64). install.py writes the keys file and the two token files to the paths each package reads at startup, all mode 600. Bundling the Google Docs tokens lets a new machine connect google-docs-personal and google-docs-work without the interactive OAuth flow. Without the cached token, the package falls back to a browser-redirect flow that Claude Code's stdio MCP loader can't drive, so it shows "Failed to connect" until the user runs the npx command manually. Make target: install-mcp. Plaintext secrets and the decrypted keys file are gitignored.
* chore(ai): initialize project notes and Claude tooling surfacesCraig Jennings2026-05-0670-0/+11660
| | | | Replace the seed notes.org with project-specific context (layout, install modes, task tracker location, recent inflection point). Bring in the synced template surfaces (protocols, workflows, scripts, references, retrospectives, someday-maybe) as tracked content for this content/documentation project.
* chore(claude): bump skillListingBudgetFraction to 5%Craig Jennings2026-05-061-2/+3
| | | | Default 1% of context maps to ~8K chars (the floor). The current skill listing totals ~10K chars and overflows on small-context models, triggering the truncation warning. Bumping to 5% leaves headroom even with the model-invocable skills' full descriptions present, and the file-based skill descriptions are now <=1000 chars each.
* chore(commands): mark user-invoked commands disable-model-invocationCraig Jennings2026-05-0617-1/+30
| | | | Add disable-model-invocation: true to the user-triggered slash commands so the harness drops their descriptions from the model's preloaded skill listing while keeping /<name> routing intact. Skills meant for model recommendation (add-tests, debug, five-whys, frontend-design, humanizer, pairwise-tests, playwright-js, playwright-py, root-cause-trace) are unchanged.
* docs(skills): tighten descriptions under 1000 charsCraig Jennings2026-05-064-3/+8
| | | | Trim each long description to <=1000 chars while preserving content (phases, when-to-use, when-not-to, companion references). Frees enough per-session skill-listing budget to quiet the truncation warning on small-context models.
* feat(languages): add typescript bundle (Vitest-canonical)Craig Jennings2026-05-061-0/+214
| | | | Mirrors the python bundle's minimal shape: one language-specific file under claude/rules/. Vitest is canonical, with brief notes for Mocha+Chai and Angular Karma legacy idioms. Covers RTL query priorities, MSW for network mocking, it.each for parametrize, async patterns, and TS-specific discipline (no any in tests, prefer satisfies, etc.).
* feat(make): fzf-pick LANG when not set, mirror project pickerCraig Jennings2026-05-061-8/+29
| | | | Add a pick_lang_shell macro that fzf-picks from $(LANGUAGES) when LANG isn't set, mirroring pick_project_shell. Wire it into install-lang and diff so both vars are now optional. Blank $(LANG) when its origin is environment, since LANG is the standard POSIX locale env var and Make would otherwise inherit "en_US.UTF-8" and bypass the picker.
* feat(hooks): make destructive-bash-confirm opt-inCraig Jennings2026-05-061-3/+16
| | | | | | | | | | I added an OPTIN_HOOKS list in the Makefile and excluded those entries from the default install-hooks recipe. destructive-bash-confirm.py is the first opt-in. The recipe now prints the exact ln -s command for each opt-in so users can wire individual ones without consulting docs. uninstall-hooks and list still iterate the full HOOKS list so they keep handling opt-ins that someone has manually linked.
* chore(claude): unwire destructive-bash-confirm hookCraig Jennings2026-05-061-11/+0
| | | | | | | Removed the PreToolUse → Bash hook entry from settings.json. The hook script stays in hooks/ for other machines that may want it. The local ~/.claude/hooks/destructive-bash-confirm.py symlink was also removed on this machine.
* docs(todo): add make-remove fzf-picker taskCraig Jennings2026-05-061-0/+1130
| | | | | | Captures the design discussion for an interactive Makefile target that lists installed rulesets and removes selected entries via fzf. Granular alternative to the bulk make uninstall.
* fix(commands): drop leftover name: frontmatter from converted commandsCraig Jennings2026-05-0613-13/+0
| | | | | | | | The conversion commit ran sed -i to strip the name: line, but the staged version from git mv was captured before the sed ran. Result: the commit shipped the original frontmatter, and the sed cleanup was sitting in the working tree. This commit lands the cleanup that should have been in aa69245.
* refactor(skills): convert 16 user-invoked skills to commandsCraig Jennings2026-05-0617-0/+0
| | | | | | | | I converted 16 user-invoked skills to commands. Skills cost ~150-300 tokens each per session for descriptions the model uses to auto-route. Commands cost nothing until you type the slash. These 16 are workflows I always trigger deliberately. The auto-routing wasn't earning its keep. This reclaims ~4-5k tokens per session. Nine skills stayed where auto-routing genuinely helps: debug, root-cause-trace, five-whys, add-tests, frontend-design, humanizer, playwright-js, playwright-py, and pairwise-tests. Pairwise-tests stays a skill because its helper files don't fit a single-file command shape. For arch-decide, I preserved the upstream MIT LICENSE alongside the command at .claude/commands/arch-decide.LICENSE so attribution stays intact.
* feat(claude): add claude config and wire it into make installCraig Jennings2026-05-065-7/+376
| | | | | | | | I moved Claude Code's user-level config into this repo so it travels with rulesets across machines instead of being machine-specific. The three pieces are settings.json, .mcp.json, and commands/refactor.md. I extended make install, uninstall, and list to handle the new .claude/ directory. The wildcard for CLAUDE_CONFIG matches both `*.json` and `.*.json` because make's glob skips dotfiles by default. Without the dot variant, .mcp.json wouldn't get picked up. I also added settings.local.json to .gitignore. That file is per-machine by convention and shouldn't ever land in the shared repo.
* feat: add humanizer skillCraig Jennings2026-05-061-0/+474
| | | | I moved this in from another repo so it sits alongside the rest of the skills here. The file content is unchanged.
* refactor(debug): make debug a triage router, align specialist cross-refsCraig Jennings2026-05-063-30/+36
| | | | I sharpened the debug skill so it stops duplicating root-cause-trace and five-whys. Phase 1 captures evidence and stops there. Phase 2 routes to the right specialist instead of asking why three times inline. Phases 3 and 4 keep the verify-and-fix discipline. I also updated the companion lines in root-cause-trace and five-whys so all three descriptions stay in sync.
* docs(commits): require @-mention of PR author in Slack notifyCraig Jennings2026-05-021-3/+3
|
* docs(commits): keep follow-up approvals terseCraig Jennings2026-05-021-0/+2
|
* docs(commits): drop "on" from Slack notify templatesCraig Jennings2026-05-021-2/+2
|
* docs(commits): add first-person voice as a personal-style passCraig Jennings2026-05-011-5/+5
| | | | | | I told Claude that the commit-message draft should sound personal, like it's coming from me. That move wasn't in the documented passes, so the drafts kept landing on impersonal third-person ("Add a test for X", "The change introduces Y"). I had to ask for the rewrite each time. First person is now the new pass (a) in all three subflows: commit messages, PR descriptions, PR review comments. The subject line and PR title stay imperative per Conventional Commits, and I left an exception for purely mechanical bodies where the subject already carries the message. I bumped the Multi-pass gate counts and the example announcement string to match.
* docs(commits): add post-review Slack notify + approve-vs-merge noteCraig Jennings2026-05-011-0/+7
| | | | | | | | Step 8 of the PR review/publish flow now sends a one-line message to channel C0AM2MWHCJU after every approve or changes-requested verdict. The new step skips humanizer and personal-style passes since the message is short and templated. The approve case sends "Approved on PR #N." with the PR URL on the next line. The changes-requested case sends "Changes Requested on PR #N" with the PR URL on the next line. I also added an explicit line saying approve doesn't authorize a merge. That keeps the merge decision with the PR author. The team's practice is approve-then-author-merges, not approve-and-merge.