From be4ed5ae6e6e7e9ba25b1f6edb435d491e175c28 Mon Sep 17 00:00:00 2001 From: JacobPEvans <20714140+JacobPEvans@users.noreply.github.com> Date: Mon, 25 May 2026 15:53:06 -0400 Subject: [PATCH 1/4] feat(wrap-up): branch on plan completeness; emit resume blocks when incomplete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Step 0 that decides whether this session's plan is actually done before running the standard cleanup pipeline. Plan identification uses the plan-mode `` already injected into the conversation as the SSOT — never mtime, which races under heavy parallelism. TaskList is session-scoped by the harness, so it is safe to read directly. A plan is complete iff every TaskList task is `completed` AND every plan-file checklist item is checked (or has clear conversation evidence of completion), or no plan file exists for this session. - Path A (plan complete or absent): identical to the prior behavior — invoke `/refresh-repo`, `/retrospecting quick`, `/clean_gone`, emit forward-looking follow-up prompt. - Path B (plan incomplete): skip Path A (refresh-repo would prune the in-flight worktree) and emit one or more Resume Blocks. Each block is a `cd ` plus a self-contained prompt that re-points at the plan file, lists remaining checklist line numbers and TaskList task IDs, and notes what is already done so a fresh session does not redo work. Items are grouped by judgement, not blindly by repo: same repo + same goal stays together, same repo + unrelated threads split, cross-repo always splits. `/wrap-up purge-pr ` is unchanged and still bypasses Step 0. Plan: ~/.claude/plans/update-wrap-up-so-that-cozy-quasar.md Assisted-by: Claude --- git-workflows/skills/wrap-up/SKILL.md | 232 +++++++++++++++++++++----- 1 file changed, 194 insertions(+), 38 deletions(-) diff --git a/git-workflows/skills/wrap-up/SKILL.md b/git-workflows/skills/wrap-up/SKILL.md index 2c7f0c5..b013b49 100644 --- a/git-workflows/skills/wrap-up/SKILL.md +++ b/git-workflows/skills/wrap-up/SKILL.md @@ -1,18 +1,93 @@ --- name: wrap-up -description: "End-of-session cleanup after PR merge: refresh repo, run quick retrospective, clean gone branches, and generate a follow-up session prompt. Combines /refresh-repo, /retrospecting quick, and /clean_gone into a single post-merge workflow, then triages remaining work into a next-session prompt and GitHub issues." +description: "End-of-session handler that first checks whether the current session's plan is actually complete. If complete: refresh repo, run quick retrospective, clean gone branches, and emit a forward-looking follow-up prompt. If incomplete: skip cleanup and emit one or more `cd ` + ready-to-paste resume prompts so the unfinished work can be picked up cold in a new session." --- -# Post-Merge Wrap-Up +# Post-Session Wrap-Up -> **State warning**: Branch state, remote tracking, and PR status change between -> invocations. Re-run all git/gh commands from Step 1. +> **State warning**: Branch state, remote tracking, TaskList contents, and plan +> checklist state all change between invocations. Re-run every git/gh command +> and re-call `TaskList` from Step 0; never trust prior outputs from this +> conversation. -Run Steps 1 and 2 **in parallel** (they are independent). Step 3 starts as soon as -Step 1 completes (depends on its remote prune). Step 4 runs after all prior steps finish. -Provide a summary of actions taken. +`/wrap-up` has two paths. Step 0 decides which one runs. -## Step 1: Refresh Repository +| Step 0 outcome | Path | +| ------------------------ | ----------------------------------------------------------------- | +| Plan complete OR no plan | **Path A** — refresh repo, retrospective, clean branches, follow-up prompt | +| Plan incomplete | **Path B** — emit resume blocks; skip Path A cleanup entirely | + +The `purge-pr` focused mode (bottom of this file) bypasses Step 0 entirely. + +## Step 0: Determine session state + +Identifying "this session's plan" must be deterministic under heavy parallelism +(many concurrent sessions writing to `~/.claude/plans/`). Filesystem heuristics +like mtime are forbidden — they race. The only signals scoped to *this* session +are the conversation transcript and harness tools whose state is session-keyed. + +### 0a. Resolve the plan file (SSOT: plan-mode system reminder) + +When a session enters plan mode, the harness injects a `` into +the conversation containing a literal `## Plan File Info:` block that names the +plan path as `/Users/jevans/.claude/plans/.md`. That path is the canonical +binding between session and plan, and it persists in conversation context after +plan mode exits. + +To resolve: + +1. Scan the current conversation context for `` blocks whose + body contains a path matching `/Users/jevans/.claude/plans/[^[:space:]]+\.md`. +2. If multiple matches exist (plan mode was re-entered against a different + file), take the **most recently quoted** one — latest in conversation order, + not by mtime. +3. If zero matches exist, this session never entered plan mode. There is no + plan file for this session. Treat the plan checklist as empty; the + completion decision then rests entirely on TaskList. + +Never search `~/.claude/plans/` by mtime, ctime, or filename pattern. Never +guess which plan belongs to this session. + +### 0b. Read the resolved plan file + +If 0a returned a path, `Read` that exact file. Extract: + +- GitHub-style checkboxes (`- [ ]` / `- [x]`) with their line numbers +- Numbered or bulleted step lists under headings such as "Step", "Phase", + "Tasks", or "Files to Change", but only when an item has an unambiguous + done/not-done signal in the file itself or in this session's conversation + +### 0c. Read the harness TaskList + +Call `TaskList`. It is intrinsically session-scoped by the harness — no +disambiguation needed. Inspect `status` per task. + +### 0d. Conversation evidence for ambiguous items + +For checklist items without an explicit `[x]`, decide based on this session's +actual evidence: file edits the assistant performed, command output, test +results visible in this conversation. Be conservative: if in doubt, treat as +incomplete. Never consult other sessions' transcripts. + +### Completion rule + +The plan is complete iff: + +- every `TaskList` task has `status == "completed"` (or the list is empty), AND +- every plan-file checklist item is checked or has clear conversation evidence + of completion (or there is no plan file at all). + +If either set has unfinished items → **Path B**. Otherwise → **Path A**. + +--- + +## Path A — Clean wrap-up (plan complete or absent) + +Run Steps A1 and A2 **in parallel** (they are independent). Step A3 starts as +soon as Step A1 completes (depends on its remote prune). Step A4 runs after +all prior steps finish. Provide a summary of actions taken. + +### A1. Refresh Repository Invoke `/refresh-repo` to: @@ -21,7 +96,7 @@ Invoke `/refresh-repo` to: - Clean up stale worktrees (merged PRs, `[gone]` remote branches) - Report repository state -## Step 2: Quick Retrospective +### A2. Quick Retrospective Invoke `/retrospecting quick` to capture a brief session retrospective: @@ -32,7 +107,7 @@ Invoke `/retrospecting quick` to capture a brief session retrospective: **Requires**: `claude-retrospective` plugin (external). If not installed, skip this step and note it was skipped. -## Step 3: Clean Gone Branches +### A3. Clean Gone Branches Invoke `/clean_gone` to remove any local branches whose remote tracking branch has been deleted: @@ -42,25 +117,25 @@ Invoke `/clean_gone` to remove any local branches whose remote tracking branch h **Requires**: `commit-commands` plugin (external). If not installed, skip this step and note it was skipped. -## Step 4: Follow-Up Session Prompt +### A4. Follow-Up Session Prompt After the retrospective completes (or is skipped), generate a follow-up prompt for the next session. -Scan the conversation history in **reverse chronological order**, stopping when no new items appear for ~10 consecutive messages. - -Most unfinished work surfaces near the end of a session. +Scan the conversation history in **reverse chronological order**, stopping when +no new items appear for ~10 consecutive messages. Most unfinished forward-looking +work surfaces near the end of a session. -### 4a + 4b: Gather Unfinished Work and Session Issues (parallel) +#### A4a + A4b: Gather Unfinished Work and Session Issues (parallel) Scan simultaneously for both categories: -**Unfinished work** (4a): +**Unfinished work** (A4a): - **Incomplete tasks** — anything started but not finished, or marked as TODO/FIXME during this session - **Items needing production-readiness** — code that works but needs hardening, tests, error handling, or documentation before it is production-ready - **Future work identified** — any issues, improvements, or ideas called out during the session as "later", "follow-up", "out of scope", or similar -**Session issues** (4b): +**Session issues** (A4b): - Errors (build failures, test failures, runtime errors) - Warnings (linter warnings, deprecation notices, compiler warnings) @@ -68,18 +143,18 @@ Scan simultaneously for both categories: - Workarounds applied that should be properly fixed - Tool or dependency issues encountered -### 4c: Triage Into Prompt vs GitHub Issues +#### A4c: Triage Into Prompt vs GitHub Issues Split the gathered items into two buckets: -1. **Next-session prompt** — items that are small enough to complete in a single focused session (roughly 1–3 tasks). Combine related items where possible. +1. **Next-session prompt** — items small enough to complete in a single focused session (roughly 1–3 tasks). Combine related items where possible. 2. **GitHub issues** — everything else. Before recommending new issues: - Search existing open issues with `gh issue list --state open` for duplicates - If a matching issue exists, recommend updating it instead of creating a new one - Consolidate related items into a single issue when they share a root cause - Each recommended issue should include a clear title, description, and acceptance criteria -### 4d: Output the Follow-Up Prompt +#### A4d: Output the Follow-Up Prompt Present the results in this format: @@ -108,12 +183,105 @@ Session Issues Log: If no follow-up items are found, state that explicitly — do not fabricate work. +### Path A Summary + +```text +Wrap-Up Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Plan: + Plan status: complete + Refresh: done or skipped + Retrospective: done or skipped + Branch cleanup: done or skipped + Follow-up prompt: done or skipped +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +--- + +## Path B — Resume blocks (plan incomplete) + +Skip Path A entirely. Skipping is intentional: `/refresh-repo` prunes stale +worktrees, which would delete the in-flight worktree the user needs to resume +in. + +### B1. Group remaining items + +Collect every unfinished item from Step 0 (plan checklist items still +unchecked + `TaskList` tasks with `status != "completed"`) and group them with +judgement, not by repo alone: + +- Items touching the same repo AND sharing one coherent goal → **one block** +- Items touching the same repo but addressing unrelated concerns → **separate + blocks**, so a fresh session is not polluted by an unrelated thread +- Items touching different repos → **separate blocks** +- If block X must finish before block Y can start, order X first and record + the dependency on Y's header + +For each block, resolve the working directory: + +1. If the block's tasks name file paths inside a worktree, use that worktree + root (`git -C rev-parse --show-toplevel`). +2. Otherwise, derive from the plan file's "Files to Change" / "File to modify" + section. +3. Last resort: the cwd at wrap-up time. + +### B2. Emit each block + +Print blocks in dependency order. Each block must be copy-pasteable into a +fresh terminal + new Claude session and runnable cold — the new session sees +none of this conversation. + +```text +Resume Block N of M — +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Working dir: + cd + +Resume prompt: +────────────────────────────── +.md + - Exact remaining checklist items with plan-file line numbers + - Any TaskList task IDs still pending and their subjects + - Relevant file paths from the plan + - One-line "already done this session" so the new session does not redo work> +────────────────────────────── + +Depends on: +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +The resume prompt restates the goal explicitly. It must never say "continue +what you were doing" or reference "this session" — the new session has no +memory of it. + +### Path B Summary + +After all resume blocks, print: + +```text +Wrap-Up Summary (incomplete) +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Plan: + Plan status: incomplete ( open checklist items, open TaskList items) + Refresh: skipped — plan incomplete + Retrospective: skipped — plan incomplete + Branch cleanup: skipped — plan incomplete + Resume blocks: +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +--- + ## Focused Mode: Purge a Specific PR Invoke as `/wrap-up purge-pr ` to close one PR and atomically -purge all local state for its branch. Skips Steps 1–4 above. Use when you -know a PR should be closed (obsolete duplicate, workaround anti-pattern, -abandoned work) and you want the local trace gone in one operation. +purge all local state for its branch. **Bypasses Step 0 and both paths above.** +Use when you know a PR should be closed (obsolete duplicate, workaround +anti-pattern, abandoned work) and you want the local trace gone in one +operation. Sequence: @@ -135,21 +303,9 @@ worktree-removal command shape from `/troubleshoot-worktree` and aligns with ## Related Skills -- **refresh-repo** (github-workflows) — PR readiness check + repo sync + worktree cleanup (Step 1 dependency); also provides `--sweep` and `--prune-stale` modes +- **refresh-repo** (github-workflows) — PR readiness check + repo sync + + worktree cleanup (Path A Step A1 dependency); also provides `--sweep` and + `--prune-stale` modes - **shape-issues** (github-workflows) — Shape and create well-structured GitHub issues - **troubleshoot-worktree** (git-workflows) — Worktree-removal command shape reused by `purge-pr` mode - **pr-standards** (git-standards) — Workaround Classification rubric used to decide when `purge-pr` is the right action - -## Summary - -Report what was completed: - -```text -Wrap-Up Summary -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Refresh: done or skipped - Retrospective: done or skipped - Branch cleanup: done or skipped - Follow-up prompt: done or skipped -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -``` From f204dff7f420dd253f2de8cabdef2a3ddb25316a Mon Sep 17 00:00:00 2001 From: JacobPEvans <20714140+JacobPEvans@users.noreply.github.com> Date: Mon, 25 May 2026 16:16:07 -0400 Subject: [PATCH 2/4] fix(wrap-up): strip angle brackets from description; require URLs for PR/issue refs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prior commit's frontmatter description contained `cd ` literally. Anthropic's plugin validator (cclint) rejects angle brackets in `description`, which surfaced as a CI validate-plugin failure. Reworded to "cd-into-worktree blocks" — same meaning, no brackets. While here, codify a URL rule the user requested for any wrap-up output that references PRs or issues: emit the full `https://github.com///(issues|pull)/` URL on first mention, never bare `#123`. The triage step now captures the `url` field from `gh issue list`, and the Resume Block template explicitly requires URLs for referenced PRs/issues. ci(precommit): add cclint as a local hook so the description-validator failure that escaped to CI on the prior commit fails at commit time instead. Uses pre-commit's `language: golang` to install `github.com/dotcommander/cclint@latest` into an isolated env — no Go toolchain on the host required. Logic lives in scripts/cclint-plugins.sh per the no-inline-scripts rule, mirroring the loop in .github/workflows/validate-plugin.yml. Verified locally: pre-commit run cclint-plugins --all-files passes on the fixed tree, and re-injecting `` into the description reproduces the exact CI error message. Assisted-by: Claude --- .pre-commit-config.yaml | 17 +++++++++++++ git-workflows/skills/wrap-up/SKILL.md | 19 +++++++++++--- scripts/cclint-plugins.sh | 36 +++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 4 deletions(-) create mode 100755 scripts/cclint-plugins.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f7e4ad7..331498f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -83,3 +83,20 @@ repos: language: system pass_filenames: false files: 'skills/.*SKILL\.md$' + + # --------------------------------------------------------------------------- + # Anthropic plugin validator (cclint) — mirrors .github/workflows/validate-plugin.yml + # so frontmatter issues (e.g. angle brackets in `description`) fail at commit time + # instead of only on CI. Pre-commit installs cclint into an isolated Go env via + # `language: golang`; no Go toolchain on the host is required. + # --------------------------------------------------------------------------- + - repo: local + hooks: + - id: cclint-plugins + name: Validate plugin frontmatter (cclint) + language: golang + additional_dependencies: + - github.com/dotcommander/cclint@latest + entry: bash scripts/cclint-plugins.sh + pass_filenames: false + files: '^[^/]+/((skills|commands|agents)/.*\.md$|\.claude-plugin/plugin\.json$)' diff --git a/git-workflows/skills/wrap-up/SKILL.md b/git-workflows/skills/wrap-up/SKILL.md index b013b49..9a34902 100644 --- a/git-workflows/skills/wrap-up/SKILL.md +++ b/git-workflows/skills/wrap-up/SKILL.md @@ -1,6 +1,6 @@ --- name: wrap-up -description: "End-of-session handler that first checks whether the current session's plan is actually complete. If complete: refresh repo, run quick retrospective, clean gone branches, and emit a forward-looking follow-up prompt. If incomplete: skip cleanup and emit one or more `cd ` + ready-to-paste resume prompts so the unfinished work can be picked up cold in a new session." +description: "End-of-session handler that first checks whether the current session's plan is actually complete. If complete: refresh repo, run quick retrospective, clean gone branches, and emit a forward-looking follow-up prompt. If incomplete: skip cleanup and emit one or more `cd`-into-worktree blocks paired with ready-to-paste resume prompts so the unfinished work can be picked up cold in a new session." --- # Post-Session Wrap-Up @@ -149,11 +149,20 @@ Split the gathered items into two buckets: 1. **Next-session prompt** — items small enough to complete in a single focused session (roughly 1–3 tasks). Combine related items where possible. 2. **GitHub issues** — everything else. Before recommending new issues: - - Search existing open issues with `gh issue list --state open` for duplicates + - Search existing open issues with `gh issue list --state open --json number,title,url` + for duplicates. **Capture the `url` field** — every issue or PR referenced in the + output below must be a full URL the user can click, never a bare `#123`. - If a matching issue exists, recommend updating it instead of creating a new one - Consolidate related items into a single issue when they share a root cause - Each recommended issue should include a clear title, description, and acceptance criteria +**URL rule (applies to every section that mentions a PR or issue):** always +emit the full `https://github.com///(issues|pull)/` URL on +first reference. Bare `#123` or `PR 123` references are forbidden because they +force the reader to guess the repo from context. If the same number appears +again in the same block, a bare `#123` is acceptable as a short reference +after the URL has been shown once. + #### A4d: Output the Follow-Up Prompt Present the results in this format: @@ -169,8 +178,8 @@ Recommended prompt for next session: Recommended GitHub Issues: ────────────────────────────── -1. — <one-line summary> [new | update #123] -2. <Title> — <one-line summary> [new | update #456] +1. <Title> — <one-line summary> [new | update https://github.com/<owner>/<repo>/issues/123] +2. <Title> — <one-line summary> [new | update https://github.com/<owner>/<repo>/pull/456] ... ────────────────────────────── @@ -246,6 +255,8 @@ Resume prompt: - Exact remaining checklist items with plan-file line numbers - Any TaskList task IDs still pending and their subjects - Relevant file paths from the plan + - Full URLs for any referenced PR or issue (e.g. + https://github.com/<owner>/<repo>/pull/123) — never a bare #123 - One-line "already done this session" so the new session does not redo work> ────────────────────────────── diff --git a/scripts/cclint-plugins.sh b/scripts/cclint-plugins.sh new file mode 100755 index 0000000..3074338 --- /dev/null +++ b/scripts/cclint-plugins.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Validate every plugin's SKILL.md / command / agent frontmatter against +# Anthropic's plugin validator using `cclint`. Mirrors the loop in +# .github/workflows/validate-plugin.yml so failures land at commit time, not +# only on CI. +# +# Pre-commit invokes this via `language: golang` with cclint listed as an +# `additional_dependencies` entry, so `cclint` is on PATH when the script runs. +# Outside pre-commit, install with: go install github.com/dotcommander/cclint@latest +set -euo pipefail + +if ! command -v cclint >/dev/null 2>&1; then + echo "cclint not on PATH. Install with: go install github.com/dotcommander/cclint@latest" >&2 + exit 1 +fi + +# Iterate every top-level directory that looks like a plugin (has plugin.json). +shopt -s nullglob +fail=0 +for plugin_dir in */; do + plugin_name="${plugin_dir%/}" + plugin_json="${plugin_dir}.claude-plugin/plugin.json" + [[ -f "${plugin_json}" ]] || continue + + # Skip hooks-only plugins (CI does the same): nothing for cclint to read. + lintable=$(jq -r '(.skills//[])+(.commands//[])+(.agents//[])|length' "${plugin_json}" 2>/dev/null || echo 0) + if [[ "${lintable}" -eq 0 ]]; then + continue + fi + + if ! cclint "${plugin_name}"; then + fail=1 + fi +done + +exit "${fail}" From 11700e42e2b32d7a9b2fb98fc9d7bef32b39437c Mon Sep 17 00:00:00 2001 From: JacobPEvans <20714140+JacobPEvans@users.noreply.github.com> Date: Mon, 25 May 2026 16:21:29 -0400 Subject: [PATCH 3/4] =?UTF-8?q?fix(wrap-up):=20generalize=20plan-path=20re?= =?UTF-8?q?ferences=20=E2=80=94=20no=20user-specific=20homes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gemini-code-assist flagged three SKILL.md sites that hardcoded `/Users/jevans/.claude/plans/...` paths and regex patterns. The plugin is published to the public marketplace, and the workspace secrets-policy bans committed user-specific paths. The matching regex in 0a would also never fire for any other plugin user. - Frontmatter Step 0a prose: describe the path shape generically (`<HOME>/.claude/plans/<slug>.md`) instead of naming one home. - 0a regex: `[^[:space:]]+/\.claude/plans/[^[:space:]]+\.md` — matches any absolute path ending under `.claude/plans/`, not just one user's home. - Path B Resume Block template: example path is `~/.claude/plans/<slug>.md` with a note to substitute the resolved absolute path emitted by the plan-mode system reminder. Refs: gemini-code-assist review threads on https://github.com/JacobPEvans/claude-code-plugins/pull/329 Assisted-by: Claude <noreply@anthropic.com> --- git-workflows/skills/wrap-up/SKILL.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/git-workflows/skills/wrap-up/SKILL.md b/git-workflows/skills/wrap-up/SKILL.md index 9a34902..97cb99d 100644 --- a/git-workflows/skills/wrap-up/SKILL.md +++ b/git-workflows/skills/wrap-up/SKILL.md @@ -30,14 +30,17 @@ are the conversation transcript and harness tools whose state is session-keyed. When a session enters plan mode, the harness injects a `<system-reminder>` into the conversation containing a literal `## Plan File Info:` block that names the -plan path as `/Users/jevans/.claude/plans/<slug>.md`. That path is the canonical -binding between session and plan, and it persists in conversation context after -plan mode exits. +plan path as an absolute path under the user's home directory, of the shape +`<HOME>/.claude/plans/<slug>.md` (for example `~/.claude/plans/<slug>.md` +expanded). That path is the canonical binding between session and plan, and it +persists in conversation context after plan mode exits. To resolve: 1. Scan the current conversation context for `<system-reminder>` blocks whose - body contains a path matching `/Users/jevans/.claude/plans/[^[:space:]]+\.md`. + body contains a path matching the regex + `[^[:space:]]+/\.claude/plans/[^[:space:]]+\.md` — any absolute path ending + under `.claude/plans/`, not just one specific user's home directory. 2. If multiple matches exist (plan mode was re-entered against a different file), take the **most recently quoted** one — latest in conversation order, not by mtime. @@ -251,7 +254,8 @@ Resume prompt: ────────────────────────────── <Self-contained prompt for this block. Must include: - Plan file path so the new session can re-enter plan mode against it: - /Users/jevans/.claude/plans/<slug>.md + ~/.claude/plans/<slug>.md (use the resolved absolute path emitted by the + plan-mode system reminder, not this literal example) - Exact remaining checklist items with plan-file line numbers - Any TaskList task IDs still pending and their subjects - Relevant file paths from the plan From 84b216bebe00d61273d57ad4d30bbe94087028a3 Mon Sep 17 00:00:00 2001 From: JacobPEvans <20714140+JacobPEvans@users.noreply.github.com> Date: Mon, 25 May 2026 17:35:34 -0400 Subject: [PATCH 4/4] refactor(precommit): drop cclint wrapper script; use cclint --staged MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three corrections after review: 1. SKILL.md prose conflated "the plan file" with "the system reminder". The plan file at ~/.claude/plans/<slug>.md is the real document on disk and the canonical store of plan content; the plan-mode <system-reminder> is the deterministic *binding* from session to file, which is what the parallelism-safe lookup needs. Step 0's intro now separates the two concepts and points Step 0a at the binding problem specifically. 2. scripts/cclint-plugins.sh was unnecessary. cclint ships a --staged git-integration mode designed for pre-commit (and a --diff mode for uncommitted changes). It reads files from the git index, detects each file's plugin context (e.g. <plugin>/skills/<name>/SKILL.md), and validates accordingly. Bare `cclint` from the repo root only scans the root-level agents/commands/skills/output-styles dirs and silently misses per-plugin SKILL.md issues — confirmed by injecting `<repo>` into a description and watching `cclint` pass while `cclint --staged` correctly fails. Hook entry is now `cclint --staged`, no wrapper. 3. The trigger glob covered commands/agents, but only skills are an active plugin component in this repo. Glob narrowed to `^[^/]+/(skills/.*\.md$|\.claude-plugin/plugin\.json$)`. Verified by staging a description with literal `<repo>` and attempting `git commit`: hook fails with the exact CI error message and blocks the commit. Reverting the description and re-committing succeeds. Assisted-by: Claude <noreply@anthropic.com> --- .pre-commit-config.yaml | 24 ++++++++++++------ git-workflows/skills/wrap-up/SKILL.md | 33 ++++++++++++++++-------- scripts/cclint-plugins.sh | 36 --------------------------- 3 files changed, 39 insertions(+), 54 deletions(-) delete mode 100755 scripts/cclint-plugins.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 331498f..1f0cc60 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -85,18 +85,28 @@ repos: files: 'skills/.*SKILL\.md$' # --------------------------------------------------------------------------- - # Anthropic plugin validator (cclint) — mirrors .github/workflows/validate-plugin.yml - # so frontmatter issues (e.g. angle brackets in `description`) fail at commit time - # instead of only on CI. Pre-commit installs cclint into an isolated Go env via - # `language: golang`; no Go toolchain on the host is required. + # Anthropic plugin validator (cclint) — fails locally on the same checks + # .github/workflows/validate-plugin.yml runs on CI (e.g. angle brackets in + # `description`). cclint's built-in `--staged` git-integration mode + # (documented as the pre-commit entrypoint) reads files from the git + # index, detects each file's plugin context (e.g. `<plugin>/skills/<name>/ + # SKILL.md`), and validates accordingly. This avoids both the type- + # detection failures that come from passing arbitrary support files + # directly and the per-plugin loop that would otherwise need a wrapper + # script. `language: golang` installs cclint into an isolated env, so no + # host Go toolchain is required. + # + # Caveat: --staged only inspects what is currently staged in git, so + # `pre-commit run --all-files` is a near no-op for this hook. Run cclint + # directly (`cclint <plugin-dir>`) or rely on CI for full-tree validation. # --------------------------------------------------------------------------- - repo: local hooks: - id: cclint-plugins - name: Validate plugin frontmatter (cclint) + name: Validate plugin frontmatter (cclint, staged) language: golang additional_dependencies: - github.com/dotcommander/cclint@latest - entry: bash scripts/cclint-plugins.sh + entry: cclint --staged pass_filenames: false - files: '^[^/]+/((skills|commands|agents)/.*\.md$|\.claude-plugin/plugin\.json$)' + files: '^[^/]+/(skills/.*\.md$|\.claude-plugin/plugin\.json$)' diff --git a/git-workflows/skills/wrap-up/SKILL.md b/git-workflows/skills/wrap-up/SKILL.md index 97cb99d..bc9aa24 100644 --- a/git-workflows/skills/wrap-up/SKILL.md +++ b/git-workflows/skills/wrap-up/SKILL.md @@ -21,19 +21,30 @@ The `purge-pr` focused mode (bottom of this file) bypasses Step 0 entirely. ## Step 0: Determine session state -Identifying "this session's plan" must be deterministic under heavy parallelism -(many concurrent sessions writing to `~/.claude/plans/`). Filesystem heuristics -like mtime are forbidden — they race. The only signals scoped to *this* session -are the conversation transcript and harness tools whose state is session-keyed. - -### 0a. Resolve the plan file (SSOT: plan-mode system reminder) +There are two distinct things to keep straight here: + +1. **The plan file itself** — a real markdown file at + `<HOME>/.claude/plans/<slug>.md`. Plan mode writes it; the assistant reads + and edits it during implementation; it persists on disk after the session + ends. The file is the canonical store of the plan's *content* — the + checklist items, the context, the files to change. Step 0b reads it. +2. **The binding from session → plan file** — i.e. *which* of the many files + under `~/.claude/plans/` is the one this session is working against. + That binding is the hard problem under parallelism, because many sessions + may be writing files into the same directory concurrently. Filesystem + heuristics (mtime, ctime, filename slug) all race. Step 0a resolves the + binding using the only signal that is scoped to one session: the + conversation transcript. + +### 0a. Resolve which plan file belongs to this session When a session enters plan mode, the harness injects a `<system-reminder>` into the conversation containing a literal `## Plan File Info:` block that names the -plan path as an absolute path under the user's home directory, of the shape -`<HOME>/.claude/plans/<slug>.md` (for example `~/.claude/plans/<slug>.md` -expanded). That path is the canonical binding between session and plan, and it -persists in conversation context after plan mode exits. +plan file's absolute path (shape: `<HOME>/.claude/plans/<slug>.md`). That +reminder is session-local — it appears only in this session's transcript — and +it persists in conversation context after plan mode exits. So scanning the +conversation for that reminder is the deterministic way to find the plan path +for *this* session, no matter how many other sessions are running. To resolve: @@ -43,7 +54,7 @@ To resolve: under `.claude/plans/`, not just one specific user's home directory. 2. If multiple matches exist (plan mode was re-entered against a different file), take the **most recently quoted** one — latest in conversation order, - not by mtime. + not by file mtime. 3. If zero matches exist, this session never entered plan mode. There is no plan file for this session. Treat the plan checklist as empty; the completion decision then rests entirely on TaskList. diff --git a/scripts/cclint-plugins.sh b/scripts/cclint-plugins.sh deleted file mode 100755 index 3074338..0000000 --- a/scripts/cclint-plugins.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash -# Validate every plugin's SKILL.md / command / agent frontmatter against -# Anthropic's plugin validator using `cclint`. Mirrors the loop in -# .github/workflows/validate-plugin.yml so failures land at commit time, not -# only on CI. -# -# Pre-commit invokes this via `language: golang` with cclint listed as an -# `additional_dependencies` entry, so `cclint` is on PATH when the script runs. -# Outside pre-commit, install with: go install github.com/dotcommander/cclint@latest -set -euo pipefail - -if ! command -v cclint >/dev/null 2>&1; then - echo "cclint not on PATH. Install with: go install github.com/dotcommander/cclint@latest" >&2 - exit 1 -fi - -# Iterate every top-level directory that looks like a plugin (has plugin.json). -shopt -s nullglob -fail=0 -for plugin_dir in */; do - plugin_name="${plugin_dir%/}" - plugin_json="${plugin_dir}.claude-plugin/plugin.json" - [[ -f "${plugin_json}" ]] || continue - - # Skip hooks-only plugins (CI does the same): nothing for cclint to read. - lintable=$(jq -r '(.skills//[])+(.commands//[])+(.agents//[])|length' "${plugin_json}" 2>/dev/null || echo 0) - if [[ "${lintable}" -eq 0 ]]; then - continue - fi - - if ! cclint "${plugin_name}"; then - fail=1 - fi -done - -exit "${fail}"