From e6386c0e89ede08298b079fcd9a348a967453348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pe=C3=B1a?= Date: Fri, 31 Jul 2026 12:54:42 -0600 Subject: [PATCH 1/4] =?UTF-8?q?feat(skills):=20sync-advisor=20=E2=80=94=20?= =?UTF-8?q?measure=20the=20drift=20before=20naming=20the=20fix=20(v1.37.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames `rebase-advisor` to `sync-advisor` and turns a blind router into an advisor that measures. The old skill was 43 lines and measured nothing. Its step 1 was "Confirm the user wants a full team sync" -- a question back to the user about something three git commands answer. Its description then over-routed: it triggered on "align with develop" and "branches are behind", both of which are `git pull`, and sent them to /make-no-mistakes:rebase, which stashes every worktree, rebases every local branch and auto-merges PRs. Between `git pull` and that the toolkit offered nothing, and nothing read-only at all. Six read-only predicates now run before anything is named: distance, fast-forward possible, dirty tree split by stage, untracked files the ref already tracks, worktrees behind, and branches with unpushed commits. The fifth is the threshold between a plain pull and the team command. The fourth is the one nothing else reports, and it was verified on a throwaway pair of repos rather than asserted. An untracked local file at a path the ref tracks aborts the pull outright while being invisible everywhere else: `git status` shows a plain `?? newfile.txt`, distance reports a clean 0 ahead 1 behind, and `merge-base --is-ancestor` says a fast-forward is possible. The pull then exits 1 with "The following untracked working tree files would be overwritten by merge ... Please move or remove them before you merge" -- the message names the user's own file and offers deletion as the remedy, which is the one irreversible move available. The skill reports these by name and recommends copying them out of the repo, never deleting them. Both controls were run: the pipeline printed nothing before the collision existed and named the file after. It never acts. Every fix is printed for the user to run. The single write is `git fetch origin --quiet`, which touches remote-tracking refs and nothing else, and the skill says so out loud -- without it every measurement is taken against a stale origin/ and reports a drift that stopped being true days ago, which is the failure this skill exists to catch. Adds `syncAdvisor.governedPaths` to make-no-mistakes.config.json: the paths whose changes get reported by name, turning "you are 12 behind" into "three hooks changed, two of them fix defects you may be looking at right now". No default -- with the key unset the skill drops the consequence line rather than falling back to a built-in list, which would be wrong in every repo but the one it was copied from and would read as measured. commands/rebase.md is untouched and stays a real destination. What changed is who decides when it applies. Origin (2026-07-31, as reported): a developer filed two bug reports against a hook with clean reproductions. One was a real defect; the other described behaviour fixed days earlier against a stale checkout, and nothing in the report separated them. Suite: 337/337 hooks, 60/60 vitest. Created by Claude Code on behalf of @lapc506 --- .claude-plugin/marketplace.json | 4 +- .claude-plugin/plugin.json | 2 +- CHANGELOG.md | 70 ++++- README.md | 24 +- commands/make-no-mistakes.config.example.json | 7 +- package.json | 2 +- skills/rebase-advisor/SKILL.md | 43 --- skills/sync-advisor/SKILL.md | 267 ++++++++++++++++++ 8 files changed, 368 insertions(+), 51 deletions(-) delete mode 100644 skills/rebase-advisor/SKILL.md create mode 100644 skills/sync-advisor/SKILL.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 2d18392..bc8cff8 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,7 +1,7 @@ { "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", "name": "make-no-mistakes", - "version": "1.36.0", + "version": "1.37.0", "description": "The disciplined dev lifecycle \u2014 implement issues, review PRs, sync releases, test E2E, manage sessions, and stash secrets via OS-native prompts. One plugin to make no mistakes.", "owner": { "name": "Luis Andres Pena Castillo", @@ -11,7 +11,7 @@ { "name": "make-no-mistakes", "description": "Dev lifecycle orchestrator. Start with /make-no-mistakes:domain-driven-advisor \u2014 the canonical entry point that inspects your repo, asks a few plain-language questions, and routes you across the six-family audit engine (schema-drift, contract-drift, ddd, explicit-architecture, strangler, enforcement-hooks), then runs a premortem on the aggregated remediation plan. Also ships disciplined Linear issue execution with worktree isolation, PR review with Greptile gating, team release sync, E2E test generation/execution, test suite previewer, security pentesting, MoSCoW + RICE prioritization, cross-platform secret stash via OS-native GUI prompts (zenity / kdialog / osascript / Get-Credential), and session management. 37 commands, 11 auto-activating skills, 2 specialized agents.", - "version": "1.36.0", + "version": "1.37.0", "author": { "name": "Luis Andres Pena Castillo", "email": "lapc506@users.noreply.github.com" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 5b89844..b1337fb 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "make-no-mistakes", - "version": "1.36.0", + "version": "1.37.0", "description": "The disciplined dev lifecycle — implement issues, review PRs, sync releases, test E2E, manage sessions, stash secrets, and enforce manifest-driven tool-call hooks. One plugin to make no mistakes.", "author": { "name": "Luis Andres Pena Castillo", diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ff9ef5..e76bf45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,72 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.37.0] - 2026-07-31 + +### Changed +- **`rebase-advisor` → `sync-advisor`, and it now measures instead of asking.** + The old skill was 43 lines and measured nothing: step 1 was *"Confirm the user + wants a full team sync"* — a question back to the user about something three + git commands answer. Its `description` then over-routed, triggering on + `"align with develop"` and `"branches are behind"` — both `git pull` cases — + and sending them all to `/make-no-mistakes:rebase`, which stashes every + worktree, rebases every local branch and auto-merges PRs. Between `git pull` + and that, the toolkit offered nothing, and nothing read-only at all. + + **Six read-only predicates** now run before anything is named: distance + (`git rev-list --left-right --count`), whether a fast-forward is possible + (`git merge-base --is-ancestor`), a dirty tree split into staged vs unstaged, + untracked files that the base ref already tracks, worktrees behind, and + branches carrying unpushed commits. The fifth is the threshold that decides + between a plain pull and the team command: one branch behind is a pull; + several worktrees behind is what `/rebase` was built for. + + **The fourth predicate is the one nothing else reports.** An untracked local + file at a path the ref tracks aborts the pull outright, and it is invisible + everywhere else — verified on a throwaway pair of repos: `git status` shows + only `?? newfile.txt`, distance reports a clean `0 ahead, 1 behind`, and + `git merge-base --is-ancestor` says a fast-forward is possible. The pull then + exits 1 with *"The following untracked working tree files would be + overwritten by merge … Please move or remove them before you merge"* — the + message names the user's own file and offers deletion as the remedy, which is + the one irreversible move available. The skill reports these by name and + recommends copying them out of the repo; it never recommends deleting them. + + **It never acts.** Every fix is printed for the user to run — `git pull`, + `git stash`, `git rebase`, `/make-no-mistakes:rebase`. The single write is + `git fetch origin --quiet`, which touches remote-tracking refs and nothing + else, and the skill says so out loud when it runs: without it every + measurement is taken against a stale `origin/` and reports a drift that + stopped being true days ago, which is the failure the skill exists to catch. + + `commands/rebase.md` is untouched and stays a real destination. What changed + is who decides when it applies — measured, not asked. + +### Added +- **`syncAdvisor.governedPaths` in `make-no-mistakes.config.json`** (see + `commands/make-no-mistakes.config.example.json`) — the paths whose changes + `sync-advisor` reports **by name**, turning "you are 12 commits behind" into + "three hooks changed, two of them fix defects you may be looking at right + now". Repo-relative prefixes, fed to + `git diff --name-only HEAD...origin/ -- ` (three dots, so it + diffs from the merge base and shows what landed on the ref rather than what + the user changed locally). + + **No default, and the degraded path omits the line rather than guessing.** + With the key unset the skill still reports distance and routing and simply + drops the consequence section. A built-in fallback list would be wrong in + every repo but the one it was copied from — and it would read as measured, + which is worse than a missing section. The key lives in the behaviour config + rather than in a domain config for the reason that file's own `_boundary` + note gives: a preference parked in a domain config is invisible to every + command that does not touch that domain. + + Origin (2026-07-31, as reported): a developer filed two bug reports against a + hook, both with clean reproductions. One was a real defect; the other + described behaviour fixed days earlier against a stale checkout, and nothing + in the report separated them. A commit count alone would not have separated + them either — naming the changed hooks would have. + ## [1.36.0] - 2026-07-31 ### Added @@ -766,7 +832,9 @@ installed caches) but had no representation on `main`; this release lands them. - Product Owner Extension (SPOPC) roadmap section in README ([PR #4](https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/pull/4)). -[Unreleased]: https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/compare/v1.35.0...HEAD +[Unreleased]: https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/compare/v1.37.0...HEAD +[1.37.0]: https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/releases/tag/v1.37.0 +[1.36.0]: https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/releases/tag/v1.36.0 [1.35.0]: https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/releases/tag/v1.35.0 [1.34.0]: https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/releases/tag/v1.34.0 [1.33.0]: https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/releases/tag/v1.33.0 diff --git a/README.md b/README.md index 12f9b54..45f5f21 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # make-no-mistakes -**Version: 1.36.0** · [CHANGELOG](./CHANGELOG.md) · [Marketplace](https://github.com/DojoCodingLabs/make-no-mistakes-toolkit) +**Version: 1.37.0** · [CHANGELOG](./CHANGELOG.md) · [Marketplace](https://github.com/DojoCodingLabs/make-no-mistakes-toolkit) The disciplined dev lifecycle — implement issues, review PRs, sync releases, test E2E, and manage sessions. One plugin to make no mistakes. @@ -128,7 +128,7 @@ Auto-activate by context — you don't need to remember the command name. | [`spike-recommend`](skills/spike-recommend/SKILL.md) | Paste a Linear issue URL or ask to analyze an issue | | [`review-open-prs`](skills/review-open-prs/SKILL.md) | Ask about open PRs, merge readiness, or Greptile scores | | [`review-active-issues`](skills/review-active-issues/SKILL.md) | Ask about your Linear issues, backlog, or issue status | -| [`rebase-advisor`](skills/rebase-advisor/SKILL.md) | Mention needing to sync branches after a release (suggests `/make-no-mistakes:rebase`) | +| [`sync-advisor`](skills/sync-advisor/SKILL.md) | Ask "am I up to date", "is my checkout stale", or mention syncing after a release. Measures the drift with six read-only git predicates, names which **governed** files changed, and routes to the smallest fix — `git pull`, a single-branch rebase, or `/make-no-mistakes:rebase`. Never syncs anything itself | | [`audit-engine`](skills/audit-engine/SKILL.md) | Run any of the six repo-health audits (schema-drift, contract-drift, ddd, explicit-architecture, strangler, enforcement-hooks). Hybrid LLM-first detection + deterministic verification + cure-mapping | | [`domain-driven-advisor`](skills/domain-driven-advisor/SKILL.md) | Ask "which audit do I need?" / "where do I start with repo health?" — routes you to the right audit(s) and runs a premortem | | [`premortem`](skills/premortem/SKILL.md) | Say "premortem this", "what could kill this", "stress test this plan", "what am I missing", or "find the blind spots" on a plan/launch/decision | @@ -262,6 +262,26 @@ The plugin reads `linear-setup.json` at your repo root for project-specific sett If no `linear-setup.json` exists, the plugin auto-detects settings from your environment (GitHub org from current repo, Linear team from MCP, etc.). +Toolkit *behaviour* (as opposed to where things go) is read from an optional +`make-no-mistakes.config.json`, also at the repo root — see +[`commands/make-no-mistakes.config.example.json`](commands/make-no-mistakes.config.example.json) +for every key and its default. Two consumers today: `language` / `diacritics` + +`explain` for `/explain`, and `syncAdvisor.governedPaths` for the `sync-advisor` +skill: + +```json +{ + "syncAdvisor": { + "governedPaths": [".claude/hooks/", "scripts/", ".github/workflows/"] + } +} +``` + +Those are the directories where a stale checkout silently changes behaviour, so +`sync-advisor` names them by file when they have moved on the base ref. The key +has **no default** — unset, the skill reports distance and routing and omits +that line rather than guessing at paths it cannot know. + ## Verify Installation **Claude Code:** diff --git a/commands/make-no-mistakes.config.example.json b/commands/make-no-mistakes.config.example.json index 677fe51..027fb04 100644 --- a/commands/make-no-mistakes.config.example.json +++ b/commands/make-no-mistakes.config.example.json @@ -14,5 +14,10 @@ "actionables": true, "askUserQuestion": "when-open-decisions" }, - "_explain": "Per-command overrides for /explain. `askUserQuestion` accepts 'when-open-decisions' (default — only call it if the explanation actually leaves a decision open), 'always', or 'never'. Setting 'always' is discouraged: a menu invented to satisfy the format trains the reader to ignore the menus that matter." + "_explain": "Per-command overrides for /explain. `askUserQuestion` accepts 'when-open-decisions' (default — only call it if the explanation actually leaves a decision open), 'always', or 'never'. Setting 'always' is discouraged: a menu invented to satisfy the format trains the reader to ignore the menus that matter.", + + "syncAdvisor": { + "governedPaths": [".claude/hooks/", "scripts/", ".github/workflows/"] + }, + "_syncAdvisor": "Paths whose changes the `sync-advisor` skill reports by name when it measures drift, so the answer is 'you are missing these three hook fixes' instead of 'you are 12 commits behind'. Repo-relative path prefixes, passed to `git diff --name-only HEAD...origin/ -- `. No default — with this key unset the skill still reports distance and routing, and omits the consequence line rather than guessing. Guessing is the worse failure: a built-in list would be wrong in every repo but the one it was copied from, and read as measured. Set it to the directories where a stale checkout silently changes behaviour — hooks, CI workflows, scripts, migrations, lockfiles." } diff --git a/package.json b/package.json index db8b325..d894e6b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lapc506/make-no-mistakes", - "version": "1.36.0", + "version": "1.37.0", "description": "The disciplined dev lifecycle — implement issues, review PRs, sync releases, test E2E, manage sessions, stash secrets, and enforce manifest-driven tool-call hooks (no SSH+DB, no manual prod, no minified build, no secret leaks, Slack format). OpenCode + Claude Code plugin.", "type": "module", "main": "./dist/index.js", diff --git a/skills/rebase-advisor/SKILL.md b/skills/rebase-advisor/SKILL.md deleted file mode 100644 index 22ea592..0000000 --- a/skills/rebase-advisor/SKILL.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -name: rebase-advisor -description: > - Detects when the user needs to sync branches after a release or rebase - their work across multiple branches. Suggests the /make-no-mistakes:rebase command. - Triggers on: "sync with release", "rebase all branches", "align with develop", - "post-release sync", "branches are behind", "need to rebase everything", - "team rebase", "release sync", "sync after merge to main". - Does NOT trigger on: single-branch rebase ("rebase this branch"), - git rebase interactive, cherry-pick, or simple merge operations. ---- - -# Rebase Advisor - -You detected that the user needs a **team-wide branch sync** — not a simple single-branch rebase. - -## When This Applies - -This skill activates when the user describes a situation involving: -- Multiple branches that need to be rebased after a release -- Post-release synchronization across the team -- Branches that have fallen behind `develop` or `main` -- Worktrees that need alignment after merges - -## What To Do - -1. Confirm the user wants a full team sync (not just rebasing one branch) -2. Suggest the dedicated command: - -> This sounds like a team-wide release sync. Use: -> -> `/make-no-mistakes:rebase {repo-name}` -> -> This command handles the full protocol: -> - Syncs `develop` and `main` with remote -> - Stashes uncommitted work in all worktrees -> - Rebases every local branch onto updated `develop` -> - Auto-merges ready PRs (CI green + Greptile approved) -> - Post-merge re-check for cascading effects -> - Restores stashed work -> - Produces a health report - -3. If the user just needs a simple single-branch rebase, do NOT invoke this skill — let them use `git rebase` directly. diff --git a/skills/sync-advisor/SKILL.md b/skills/sync-advisor/SKILL.md new file mode 100644 index 0000000..405e7d1 --- /dev/null +++ b/skills/sync-advisor/SKILL.md @@ -0,0 +1,267 @@ +--- +name: sync-advisor +description: > + Measures how far a checkout has drifted from its base ref and routes to the + smallest sync that closes the gap — a plain pull, a single-branch rebase, or + the team-wide /make-no-mistakes:rebase. Reports which governed files changed, + so the answer is "you are missing these three hook fixes", not just "you are + N commits behind". Read-only: it measures and recommends, it never syncs. + Triggers on: "am I up to date", "is my branch behind", "how far behind am I", + "do I need to pull", "is my checkout stale", "what changed on develop", + "sync with develop", "sync with release", "post-release sync", + "rebase all branches", "team rebase", "my worktrees are behind", + "did I miss anything from develop". + Does NOT trigger on: interactive rebase ("git rebase -i", "squash these + commits"), cherry-pick, resolving a named merge conflict, or a request to + actually perform the team sync (that is /make-no-mistakes:rebase itself). +--- + +# Sync Advisor + +A checkout has drifted from its base ref, or may have. **Measure the drift +before naming a fix.** Six read-only predicates answer what would otherwise be +a question back to the user, and the answer separates three very different +routes: `git pull`, a single-branch rebase, and `/make-no-mistakes:rebase` — +which stashes every worktree, rebases every local branch, and auto-merges PRs. + +Between `git pull` and that, there was nothing. Filling that gap is what this +skill is for, and it is why it does not forward every mention of "sync" to the +heaviest command available. + +## This skill never acts + +Every command below either reads or reports. The fixes are **printed for the +user to run**, never executed: + +- `git pull`, `git rebase`, `git stash`, `git merge` — suggested, not run. +- `/make-no-mistakes:rebase` — suggested, not invoked. + +One exception, and name it out loud when you use it: `git fetch origin --quiet` +runs first. It writes remote-tracking refs and nothing else — no local branch, +no index, no working tree. Without it every measurement below is taken against +a stale `origin/` and reports a drift that stopped being true days ago, +which is the failure this skill exists to prevent. + +If the user wants the sync performed, that is the `/make-no-mistakes:rebase` +command's job and they invoke it themselves. + +## Step 0 — Resolve the base ref + +Do not assume `develop`. Resolve in this order and say which one you used: + +```bash +git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' # 1. what this branch tracks +git symbolic-ref --short refs/remotes/origin/HEAD # 2. the remote's default branch +git show-ref --verify --quiet refs/remotes/origin/develop; echo "exit=$?" # 3. probe develop, then main +``` + +Command 1 fails on a branch with no upstream — that is information, not an +error: an unpushed branch, which predicate 6 covers. Fall through to 2, then 3. +Repos in this toolkit's audience use `develop`, `main`, `master` and `trunk`; a +hardcoded `develop` produces a confident measurement against a ref that does +not exist. + +Then fetch, so that `origin/$BASE` is the ref and not a memory of it: + +```bash +git fetch origin --quiet +``` + +## Step 1 — The six predicates + +Run all six. Each routes somewhere different, and the fourth is the one nothing +else reports. + +| # | Predicate | Answers | +|---|-----------|---------| +| 1 | Distance | Is anything needed at all? | +| 2 | Fast-forward? | `pull` vs. rebase/merge | +| 3 | Dirty tree | Stash or commit first | +| 4 | Untracked files the ref already tracks | **Back up first — these abort the pull** | +| 5 | Worktrees behind | A one-branch problem or a fleet problem | +| 6 | Unpushed commits | What a team-wide rebase would be touching | + +### 1. Distance + +```bash +git rev-list --left-right --count "HEAD...origin/$BASE" +``` + +Two tab-separated numbers: commits ahead of the ref, then commits behind it. +Two zeros means the work is already done — **say so and stop.** A skill that +always finds something to recommend is not measuring. + +### 2. Is a fast-forward possible? + +```bash +git merge-base --is-ancestor HEAD "origin/$BASE"; echo "exit=$?" +``` + +`exit=0` — HEAD is an ancestor of the ref, so `git pull --ff-only` applies and +nothing can conflict. `exit=1` — the histories diverged and the user holds +local commits the ref does not; a pull will merge or rebase, and can conflict. +Those are different recommendations, and predicate 1 alone cannot tell them +apart. + +### 3. Dirty tree, split by stage + +```bash +git status --porcelain +``` + +Column 1 is the index, column 2 is the working tree. ` M` is unstaged, `M ` is +staged, `MM` is both, `??` is untracked. The split matters because the advice +differs: staged work is one commit from safe, unstaged work needs a stash, and +the untracked rows belong to predicate 4. + +### 4. Untracked files that the ref already tracks + +The one nobody has, and the one worth the most: + +```bash +comm -12 \ + <(git ls-files --others --exclude-standard | sort) \ + <(git ls-tree -r --name-only "origin/$BASE" | sort) +``` + +Every path this prints exists locally as untracked **and** exists on the ref as +tracked. Git refuses to overwrite it, so the pull aborts before doing anything: + +``` +error: The following untracked working tree files would be overwritten by merge: + +Please move or remove them before you merge. +Aborting +``` + +Three things earn this a dedicated predicate. It does not read as a problem in +`git status`, which shows the file as a plain `??` — indistinguishable from +harmless scratch. It is invisible to the distance count, and to the +fast-forward check: a clean `0 ahead, 1 behind` fast-forward aborts exactly the +same way. And the message names the user's own files, then names deletion as a +remedy — so the reflex is to remove the local copy, the one irreversible move +available. + +Report these by name and recommend copying them somewhere outside the repo +first. Never recommend deleting them. + +### 5. Worktrees behind + +```bash +git worktree list --porcelain +``` + +Then, for each `branch refs/heads/` it reports: + +```bash +git rev-list --count "..origin/$BASE" +``` + +This works from any checkout — worktrees share one object store and one ref +namespace, so there is no need to enter each one. **This predicate is the +threshold for `/make-no-mistakes:rebase`**: one branch behind is a `git pull`; +several worktrees and branches behind is what that command was built for. + +### 6. Branches carrying unpushed commits + +```bash +git for-each-ref --format='%(refname:short) | %(upstream:short) | %(upstream:track)' refs/heads +``` + +`[ahead 3]` means three commits exist only locally. An empty upstream column +means the branch was never pushed at all — cross-check with +`git ls-remote --heads origin` when it matters. + +This does not change the recommendation; it changes what the user knows before +accepting it. `/make-no-mistakes:rebase` rebases every local branch, and this +is the list of what has no remote copy if one of those rebases goes wrong. + +## Step 2 — The consequence line + +Distance is a number. **What the user needs is what the distance costs.** Read +`make-no-mistakes.config.json` at the repo root (see +`commands/make-no-mistakes.config.example.json`): + +```json +{ + "syncAdvisor": { + "governedPaths": [".claude/hooks/", "scripts/", ".github/workflows/"] + } +} +``` + +For each configured path: + +```bash +git diff --name-only "HEAD...origin/$BASE" -- "" +git log --oneline "HEAD..origin/$BASE" -- "" +``` + +Three dots on the `diff` is deliberate: it diffs from the merge base, so it +shows what landed on the ref rather than what the user changed locally. + +Report the files with the subject line of the commit that touched each. "You +are 12 behind" is a number nobody can act on. "Three hooks changed, two of them +fix defects you may be looking at right now" is the sentence that ends the +wrong debugging session. + +**Degraded mode — no config file, or no `syncAdvisor` key.** Report distance +and routing exactly as above and **omit the consequence line entirely**. Do not +guess at governed paths and do not fall back to a built-in list: an invented +list is wrong in every repo but the one it was copied from, and a confident +wrong list is worse than a missing section. Mention once, in one clause, that +the key is unset and what setting it would add. + +## Step 3 — Route + +Take the first row that matches, in order: + +| Condition | Recommend | +|-----------|-----------| +| Behind = 0, and no worktree behind | Nothing. The checkout is current — say it in one line and stop, dirty tree or not. Uncommitted work is not a sync problem. | +| Untracked collisions (predicate 4) | Copy those files outside the repo first. Nothing else proceeds — the pull aborts. | +| Behind > 0 and dirty tree (predicate 3) | `git stash` for unstaged work, a commit for staged work — naming the files — then re-read this table. | +| Behind > 0, fast-forward possible, one branch | `git pull --ff-only` | +| Behind > 0, diverged, one branch | `git rebase origin/$BASE` — a single-branch rebase, not the team command. | +| Two or more worktrees/branches behind | `/make-no-mistakes:rebase {repo-name}` | + +`/make-no-mistakes:rebase` is a real destination and stays one. It syncs +`develop` and `main` with the remote, stashes uncommitted work across all +worktrees, rebases every local branch onto the updated base, auto-merges PRs +that are green and approved, re-checks for cascading effects, restores the +stashes, and prints a health report. That is the right tool for a fleet of +branches after a release, and the wrong tool for one branch four commits +behind. What changes here is who decides which case it is — measured, not +asked. + +## Report shape + +Keep it to what was measured: + +``` +origin/develop — 0 ahead, 12 behind. Fast-forward possible. + +Governed files that changed: + .claude/hooks/.sh "refuse a command that discards stderr" + .claude/hooks/.sh "narrow the citation matcher" + .github/workflows/.yml "pin the checkout to the exact SHA" + +Working tree: clean. Worktrees behind: none. Unpushed: none. + + git pull --ff-only +``` + +Two hooks in that list changed behaviour the user may be reporting as a bug. +Name that when it applies — it is the whole point of the consequence line. + +## Why this exists + +On 2026-07-31 a developer filed two bug reports against a hook, both with clean +reproductions. One was a real defect. The other described behaviour that had +been fixed days earlier; the checkout was stale. **Nothing in the report +separated them**, and the wrong half consumed a debugging session before the +checkout's age was ever questioned. + +A commit count alone would not have caught it either. What was needed was the +consequence line: *three hooks changed, two of them fix defects you may be +looking at right now.* From ebbf10e1f505f57e521070ebe5f76bc88becc6c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pe=C3=B1a?= Date: Fri, 31 Jul 2026 13:00:14 -0600 Subject: [PATCH 2/4] =?UTF-8?q?chore(release):=20take=201.38.0=20instead?= =?UTF-8?q?=20of=201.37.0=20=E2=80=94=20#55=20owns=201.37.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured on origin/main @ ee0ba47: the three version files all read 1.36.0, and `gh pr diff 55 | grep -E '^\+.*"version"'` shows #55 already bumps to 1.37.0 in package.json, plugin.json and marketplace.json. Taking 1.37.0 here collides with it in all three. The same probe over #52, #46, #44 and #41 returns nothing, so 1.38.0 is free. Skipping a version costs nothing; colliding does. Minor and not major, decided by reading. A skill auto-activates on its `description` rather than being invoked by name like a command, so a renamed `name:` changes no call site. The reference search returns nothing: grep -rniI "rebase.advisor" . --exclude-dir=node_modules \ --exclude-dir=.git --exclude=CHANGELOG.md -> no hits That negative is real and not a broken search -- the same grep for `spike-recommend` returns 10 files, so cross-references of this shape are found when they exist. One surface does break and is stated rather than folded in: a user who typed `/make-no-mistakes:rebase-advisor` explicitly (README:137 documents that skills can be invoked that way) now gets an unknown skill. It fails loudly, the replacement is one row away in the same table, and the installer prunes the old file rather than leaving both live. The `[1.37.0]:` CHANGELOG reference slot is deliberately left for #55 to fill. Suite: 337/337 hooks, 60/60 vitest. Created by Claude Code on behalf of @lapc506 --- .claude-plugin/marketplace.json | 4 ++-- .claude-plugin/plugin.json | 2 +- CHANGELOG.md | 22 +++++++++++++++++++--- README.md | 2 +- package.json | 2 +- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index bc8cff8..21c82e0 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,7 +1,7 @@ { "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", "name": "make-no-mistakes", - "version": "1.37.0", + "version": "1.38.0", "description": "The disciplined dev lifecycle \u2014 implement issues, review PRs, sync releases, test E2E, manage sessions, and stash secrets via OS-native prompts. One plugin to make no mistakes.", "owner": { "name": "Luis Andres Pena Castillo", @@ -11,7 +11,7 @@ { "name": "make-no-mistakes", "description": "Dev lifecycle orchestrator. Start with /make-no-mistakes:domain-driven-advisor \u2014 the canonical entry point that inspects your repo, asks a few plain-language questions, and routes you across the six-family audit engine (schema-drift, contract-drift, ddd, explicit-architecture, strangler, enforcement-hooks), then runs a premortem on the aggregated remediation plan. Also ships disciplined Linear issue execution with worktree isolation, PR review with Greptile gating, team release sync, E2E test generation/execution, test suite previewer, security pentesting, MoSCoW + RICE prioritization, cross-platform secret stash via OS-native GUI prompts (zenity / kdialog / osascript / Get-Credential), and session management. 37 commands, 11 auto-activating skills, 2 specialized agents.", - "version": "1.37.0", + "version": "1.38.0", "author": { "name": "Luis Andres Pena Castillo", "email": "lapc506@users.noreply.github.com" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index b1337fb..f2af3c5 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "make-no-mistakes", - "version": "1.37.0", + "version": "1.38.0", "description": "The disciplined dev lifecycle — implement issues, review PRs, sync releases, test E2E, manage sessions, stash secrets, and enforce manifest-driven tool-call hooks. One plugin to make no mistakes.", "author": { "name": "Luis Andres Pena Castillo", diff --git a/CHANGELOG.md b/CHANGELOG.md index e76bf45..1300ebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [1.37.0] - 2026-07-31 +## [1.38.0] - 2026-07-31 ### Changed - **`rebase-advisor` → `sync-advisor`, and it now measures instead of asking.** @@ -59,6 +59,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `commands/rebase.md` is untouched and stays a real destination. What changed is who decides when it applies — measured, not asked. + **Minor and not major, decided by reading rather than by habit.** A skill + auto-activates on its `description`; it is not invoked by name the way a + command is, so a renamed `name:` changes no call site. + `grep -rniI "rebase.advisor" . --exclude-dir=node_modules --exclude-dir=.git + --exclude=CHANGELOG.md` returns nothing — no command, no doc, no other skill + named it. That negative is real rather than a broken search: the same grep + for `spike-recommend` returns 10 files, so cross-references of this shape do + get found when they exist. + + One surface does break, and it is named here rather than folded into the + above: a user who typed `/make-no-mistakes:rebase-advisor` explicitly (README + documents that skills can be invoked that way) now gets an unknown skill. It + fails loudly, with the replacement one line away in the same table, and the + installer prunes the old file rather than leaving both live — but anyone who + reads that as breaking should say so on the PR. + ### Added - **`syncAdvisor.governedPaths` in `make-no-mistakes.config.json`** (see `commands/make-no-mistakes.config.example.json`) — the paths whose changes @@ -832,8 +848,8 @@ installed caches) but had no representation on `main`; this release lands them. - Product Owner Extension (SPOPC) roadmap section in README ([PR #4](https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/pull/4)). -[Unreleased]: https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/compare/v1.37.0...HEAD -[1.37.0]: https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/releases/tag/v1.37.0 +[Unreleased]: https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/compare/v1.38.0...HEAD +[1.38.0]: https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/releases/tag/v1.38.0 [1.36.0]: https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/releases/tag/v1.36.0 [1.35.0]: https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/releases/tag/v1.35.0 [1.34.0]: https://github.com/DojoCodingLabs/make-no-mistakes-toolkit/releases/tag/v1.34.0 diff --git a/README.md b/README.md index 45f5f21..40ea254 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # make-no-mistakes -**Version: 1.37.0** · [CHANGELOG](./CHANGELOG.md) · [Marketplace](https://github.com/DojoCodingLabs/make-no-mistakes-toolkit) +**Version: 1.38.0** · [CHANGELOG](./CHANGELOG.md) · [Marketplace](https://github.com/DojoCodingLabs/make-no-mistakes-toolkit) The disciplined dev lifecycle — implement issues, review PRs, sync releases, test E2E, and manage sessions. One plugin to make no mistakes. diff --git a/package.json b/package.json index d894e6b..55db7af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lapc506/make-no-mistakes", - "version": "1.37.0", + "version": "1.38.0", "description": "The disciplined dev lifecycle — implement issues, review PRs, sync releases, test E2E, manage sessions, stash secrets, and enforce manifest-driven tool-call hooks (no SSH+DB, no manual prod, no minified build, no secret leaks, Slack format). OpenCode + Claude Code plugin.", "type": "module", "main": "./dist/index.js", From 00e2c088b4c3ca72888404b6628908ed30ceaa85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pe=C3=B1a?= Date: Fri, 31 Jul 2026 13:18:25 -0600 Subject: [PATCH 3/4] fix(sync-advisor): make the collision predicate correct from any subdirectory Reviewer P3 on `ea8c46e` claimed `git ls-tree` "always outputs repository-relative paths" while `git ls-files` is prefix-relative, so the two would fail to match from a subdirectory, and proposed `--full-name` on `ls-files`. Measured: the premise is false and the proposed fix introduces the bug it claims to prevent. From `sub/`, `ls-tree -r --name-only` prints `newfile.txt`, not `sub/newfile.txt` -- it strips the prefix exactly like `ls-files` does, so the original command matched fine and found the collision. Adding `--full-name` alone then makes `ls-files` emit `sub/newfile.txt` against `ls-tree`'s `newfile.txt`, they stop matching, and `comm -12` returns empty -- a clean bill of health for a tree about to abort the pull. But it pointed at a real weakness of a different kind. From a subdirectory both commands are SCOPED to that subtree, so a collision at the repo root is not seen at all. That is scope, not format, and no combination of format flags fixes it: `--full-name` changes how a path prints, never which paths are considered. The pathspec does. Shipped: `--full-name -- :/` on ls-files, `--full-tree` on ls-tree. Four cases run, with the command extracted verbatim from SKILL.md so the test cannot drift from the doc -- negative control from the root and from `sub/` (both empty), positive from the root and from `sub/` with one collision in each location (both list `rootfile.txt` and `sub/newfile.txt`). Suite: 340/340 hooks, 60/60 vitest. Created by Claude Code on behalf of @lapc506 --- CHANGELOG.md | 12 ++++++++++++ skills/sync-advisor/SKILL.md | 21 +++++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f73d75d..3d83047 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 the one irreversible move available. The skill reports these by name and recommends copying them out of the repo; it never recommends deleting them. + The collision check carries `--full-name -- :/` and `--full-tree` because the + predicate is otherwise silently wrong when run from a subdirectory, in two + independent ways. **Format:** without the flags both commands print + prefix-relative paths, and adding `--full-name` on its own is worse than + adding nothing — `ls-files` emits `sub/newfile.txt` while `ls-tree` emits + `newfile.txt`, they stop matching, and `comm -12` returns empty for a tree + that is about to abort the pull. **Scope:** `--full-name` changes how a path + prints, never which paths are considered, so without `-- :/` a collision at + the repo root is invisible to a run started from `sub/`. Both controls run, + from the repo root and from a subdirectory, with the command extracted + verbatim from the SKILL.md so the test cannot drift from the doc. + **It never acts.** Every fix is printed for the user to run — `git pull`, `git stash`, `git rebase`, `/make-no-mistakes:rebase`. The single write is `git fetch origin --quiet`, which touches remote-tracking refs and nothing diff --git a/skills/sync-advisor/SKILL.md b/skills/sync-advisor/SKILL.md index 405e7d1..71048d4 100644 --- a/skills/sync-advisor/SKILL.md +++ b/skills/sync-advisor/SKILL.md @@ -120,10 +120,27 @@ The one nobody has, and the one worth the most: ```bash comm -12 \ - <(git ls-files --others --exclude-standard | sort) \ - <(git ls-tree -r --name-only "origin/$BASE" | sort) + <(git ls-files --others --exclude-standard --full-name -- :/ | sort) \ + <(git ls-tree -r --full-tree --name-only "origin/$BASE" | sort) ``` +**All three of `--full-name`, `:/` and `--full-tree` are load-bearing, and two +of them fix different problems.** Run from a subdirectory with none of them, +both commands report paths relative to the current prefix *and* restrict +themselves to that subtree — so they still agree with each other, and the +predicate quietly answers for one directory instead of the repo. + +`--full-name` and `--full-tree` fix the **format** halves, and they are a pair: +adding `--full-name` alone is worse than adding nothing, because `ls-files` +starts emitting `sub/newfile.txt` while `ls-tree` still emits `newfile.txt`, +the two stop matching, and `comm -12` returns empty — a clean bill of health +for a tree that is about to abort the pull. + +`-- :/` fixes the **scope** half, which the flags do not touch: `--full-name` +changes how a path is printed, never which paths are considered. Without the +pathspec, a collision at the repo root is invisible to a run started from +`sub/`. + Every path this prints exists locally as untracked **and** exists on the ref as tracked. Git refuses to overwrite it, so the pull aborts before doing anything: From ac770c76d55ae4dd37000b9c74aef244e08d5b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pe=C3=B1a?= Date: Fri, 31 Jul 2026 13:25:11 -0600 Subject: [PATCH 4/4] fix(sync-advisor): resolve the base ref correctly, and in a bare form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer P2 on `00e2c08`, and it is right. Step 0 returned values carrying the remote prefix while every predicate interpolates `origin/$BASE`, so the run died on `origin/origin/develop`: $ git rev-list --left-right --count "HEAD...origin/origin/main" fatal: ambiguous argument … unknown revision exit=128 Fixed by normalising unconditionally (`${BASE#refs/remotes/}`, `${BASE#origin/}`) whichever branch of the resolution produced the value. Verified both branches with a control that must fail: where `origin/HEAD` IS set it returns `origin/main`, normalises to `main`, and `origin/main` resolves; the un-normalised form exits 128. The P2 understated it. Step 0's first command resolved the base from `@{upstream}`, which on a feature branch is that branch's OWN remote copy -- here `origin/andres/sync-advisor`. That answers "am I pushed?", which is predicate 6's question, and would report 0 behind on a branch far behind the real base. Stripping a prefix would have left it pointing at the wrong ref, so the command is gone from base resolution rather than patched. Also measured while there: `git symbolic-ref --short refs/remotes/origin/HEAD` fails outright in this repo (`not a symbolic ref`), so it is documented as a fall-through into the develop/main/master/trunk probe rather than as a step that is expected to succeed. P4 (`--abbrev-ref` and `--symbolic-full-name` redundant) is also correct -- both forms return `origin/andres/sync-advisor` here -- and is moot: that command no longer appears. Suite: 340/340 hooks, 60/60 vitest. Created by Claude Code on behalf of @lapc506 --- CHANGELOG.md | 15 +++++++++++ skills/sync-advisor/SKILL.md | 50 +++++++++++++++++++++++++++++------- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d83047..5e48682 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `commands/rebase.md` is untouched and stays a real destination. What changed is who decides when it applies — measured, not asked. + Step 0 resolves the base ref in a bare form (`develop`, never + `origin/develop`) and normalises with `${BASE#origin/}` whichever branch of + the resolution produced it, because every predicate interpolates + `origin/$BASE` and a value carrying the remote becomes `origin/origin/develop` + — `fatal: ambiguous argument … unknown revision`, exit 128, measured. It also + does **not** resolve the base from `@{upstream}`: on a feature branch that is + the branch's own remote copy (`origin/andres/sync-advisor`), so it answers + "am I pushed?" — predicate 6's question — and reports `0 behind` on a branch + far behind the actual base. Wrong ref, not merely wrong spelling. The + `origin/HEAD` step is a fall-through rather than a requirement: it is + routinely unset (`fatal: ref refs/remotes/origin/HEAD is not a symbolic ref`, + observed in this repo) and the `develop`/`main`/`master`/`trunk` probe covers + it. Both branches of the resolution verified, each against a control that + fails. + **Minor and not major, decided by reading rather than by habit.** A skill auto-activates on its `description`; it is not invoked by name the way a command is, so a renamed `name:` changes no call site. diff --git a/skills/sync-advisor/SKILL.md b/skills/sync-advisor/SKILL.md index 71048d4..a09e528 100644 --- a/skills/sync-advisor/SKILL.md +++ b/skills/sync-advisor/SKILL.md @@ -47,19 +47,51 @@ command's job and they invoke it themselves. ## Step 0 — Resolve the base ref -Do not assume `develop`. Resolve in this order and say which one you used: +Two things have to be right here, and each one fails silently in its own way: +**which** branch is the base, and what **form** its name is in. + +**`$BASE` is a bare branch name — `develop`, never `origin/develop`.** Every +predicate below interpolates `origin/$BASE`, so a value that already carries +the remote becomes `origin/origin/develop`, which is not a ref and takes the +whole run down: + +``` +$ git rev-list --left-right --count "HEAD...origin/origin/develop" +fatal: ambiguous argument 'HEAD...origin/origin/develop': unknown revision +``` + +**Do not resolve the base from `@{upstream}`.** On any feature branch the +upstream is that branch's *own* remote copy — `origin/andres/sync-advisor`, not +`origin/develop`. Measuring against it answers *"am I pushed?"*, which is +predicate 6's question, and it reports `0 behind` on a branch that is fifty +commits behind the base. It is the wrong ref, not merely the wrong spelling. + +Resolve in this order, and say which one you used: ```bash -git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' # 1. what this branch tracks -git symbolic-ref --short refs/remotes/origin/HEAD # 2. the remote's default branch -git show-ref --verify --quiet refs/remotes/origin/develop; echo "exit=$?" # 3. probe develop, then main +# 1. a base the user named explicitly — take it +# 2. the remote's default branch, when origin/HEAD is set +git symbolic-ref --short refs/remotes/origin/HEAD + +# 3. otherwise probe, in order +for b in develop main master trunk; do + git show-ref --verify --quiet "refs/remotes/origin/$b" && { echo "$b"; break; } +done ``` -Command 1 fails on a branch with no upstream — that is information, not an -error: an unpushed branch, which predicate 6 covers. Fall through to 2, then 3. -Repos in this toolkit's audience use `develop`, `main`, `master` and `trunk`; a -hardcoded `develop` produces a confident measurement against a ref that does -not exist. +Step 2 is not reliable enough to stand alone — `origin/HEAD` is frequently +unset in a fresh clone or worktree, and then it fails with +`fatal: ref refs/remotes/origin/HEAD is not a symbolic ref`. That is a fall- +through, not an error to report. Repos in this toolkit's audience use +`develop`, `main`, `master` and `trunk`; a hardcoded `develop` produces a +confident measurement against a ref that does not exist. + +Then normalise unconditionally, whichever branch produced the value: + +```bash +BASE=${BASE#refs/remotes/} +BASE=${BASE#origin/} +``` Then fetch, so that `origin/$BASE` is the ref and not a memory of it: