diff --git a/.claude/commands/qa.md b/.claude/commands/qa.md index a5b76af..1dbb3d4 100644 --- a/.claude/commands/qa.md +++ b/.claude/commands/qa.md @@ -1,10 +1,9 @@ - You are executing the `/qa` skill. Your argument is: $ARGUMENTS --- @@ -159,3 +158,4 @@ After applying labels, tell the user what was done: - Never post the comment without showing it to the user first. - Never apply labels without user confirmation (the confirmation in Step 3 is the single gate for both posting the comment and applying labels). - Never merge, deploy, or take any action beyond labeling and commenting. + diff --git a/.claude/commands/release.md b/.claude/commands/release.md index 58c4a53..5642a08 100644 --- a/.claude/commands/release.md +++ b/.claude/commands/release.md @@ -1,4 +1,3 @@ - You are executing the `/release` skill. Your argument is: $ARGUMENTS --- @@ -17,9 +16,9 @@ git branch --show-current ``` **Determine mode:** -- If `` is empty → **Trunk Mode**. Follow the Trunk Mode section below. -- If `` is set but `` is empty → **Two-Branch Mode**. Follow the Two-Branch Mode section below. -- If both `` and `` are set → **Three-Branch Mode**. Follow the Three-Branch Mode section below. +- If `dev` is empty → **Trunk Mode**. Follow the Trunk Mode section below. +- If `dev` is set but `` is empty → **Two-Branch Mode**. Follow the Two-Branch Mode section below. +- If both `dev` and `` are set → **Three-Branch Mode**. Follow the Three-Branch Mode section below. --- @@ -116,9 +115,9 @@ Tell the user: ### Step 1 — Verify state -If not on ``, switch to it: +If not on `dev`, switch to it: ```bash -git checkout && git pull +git checkout dev && git pull ``` Verify a version tag exists on HEAD: @@ -130,9 +129,9 @@ If no tag is found, warn: "No version tag found on HEAD. Run `/version` first to ### Step 2 — Compute what's being promoted -Find all merge commits in `` not yet in `main`: +Find all merge commits in `dev` not yet in `main`: ```bash -git log main.. --merges --pretty=format:"%s" +git log main..dev --merges --pretty=format:"%s" ``` Parse PR numbers from the merge commit subjects. GitHub's format is: @@ -169,10 +168,10 @@ Have you tested all of the above on preview? Type 'release' to confirm. Wait for the user to type "release" or an explicit confirmation. Any other response → stop and ask what they'd like to change. -### Step 4 — Create PR: `` → `main` +### Step 4 — Create PR: `dev` → `main` ```bash -gh pr create --base main --head \ +gh pr create --base main --head dev \ --title "Release vX.Y.Z" \ --body "$(cat <<'EOF' Release vX.Y.Z @@ -260,7 +259,7 @@ Find all merge commits in `` not yet in `main`: git log main.. --merges --pretty=format:"%s" ``` -Parse PR numbers from the merge commit subjects. Note: some merge commits here may be promotion merges from `` — these are identifiable by subjects matching "Merge ... from ``". Include them in the list but note they are promotion merges; extract the original feature PRs from their PR bodies when possible. +Parse PR numbers from the merge commit subjects. Note: some merge commits here may be promotion merges from `dev` — these are identifiable by subjects matching "Merge ... from `dev`". Include them in the list but note they are promotion merges; extract the original feature PRs from their PR bodies when possible. For each PR number found, retrieve the PR body: ```bash @@ -358,3 +357,4 @@ After the command runs, note the release URL from the output. Tell the user: > "Released vX.Y.Z. Issues #N, #M closed automatically. GitHub Release vX.Y.Z created at ``. Run `make deploy-prod` to ship to production." + diff --git a/.claude/commands/review.md b/.claude/commands/review.md index de13cb9..d21fb26 100644 --- a/.claude/commands/review.md +++ b/.claude/commands/review.md @@ -1,4 +1,3 @@ - You are executing the `/review` skill. Your argument is: $ARGUMENTS --- @@ -55,3 +54,4 @@ After the review agent completes, relay its verdict to the user: - This command does not commit, push, or merge anything. It only reviews. - It may be called manually at any time, not just from `/ship`. - The review comment is posted to GitHub for the audit trail. + diff --git a/.claude/commands/setup.md b/.claude/commands/setup.md index 0fed24b..f79b347 100644 --- a/.claude/commands/setup.md +++ b/.claude/commands/setup.md @@ -1,4 +1,3 @@ - You are executing the `/setup` skill. Your argument is: $ARGUMENTS --- @@ -424,3 +423,4 @@ Add a note: `/start` can be used to create well-formed GitHub issues without wri - Never fetch more than 100 labels in a single command. `gh label list --limit 100` is the ceiling. - Do not skip any human gate in Phase 2 or Phase 3 — each write requires confirmation. - If the user skips a config value, do not ask again. Move on. + diff --git a/.claude/commands/ship.md b/.claude/commands/ship.md index 249eb63..53b3351 100644 --- a/.claude/commands/ship.md +++ b/.claude/commands/ship.md @@ -1,4 +1,3 @@ - You are executing the `/ship` skill. Your argument is: $ARGUMENTS --- @@ -24,7 +23,7 @@ git branch --show-current Determine which branches are protected (i.e., not a feature branch): - Always: `main` -- If `` is non-empty: also `` +- If `dev` is non-empty: also `dev` - If `` is non-empty: also `` If the current branch matches any of the above, **abort immediately** and say: @@ -89,7 +88,7 @@ git ls-files CODEOWNERS .github/CODEOWNERS docs/CODEOWNERS 2>/dev/null If the output is non-empty, inform the user: "CODEOWNERS file detected — GitHub will automatically request reviews from code owners." Determine the PR target branch: -- If `` is non-empty, target `` (two-branch or three-branch mode). +- If `dev` is non-empty, target `dev` (two-branch or three-branch mode). - Otherwise, target `main` (trunk mode). Determine the issue reference format: @@ -132,7 +131,7 @@ Wait for the review agent to complete and report its verdict. ## Step 7 — Act on verdict Determine merge command (used by all paths below): -- If in **trunk mode** (`` is empty): use `gh pr merge --merge` directly — `make merge` may refuse merges targeting `main`. +- If in **trunk mode** (`dev` is empty): use `gh pr merge --merge` directly — `make merge` may refuse merges targeting `main`. - Otherwise: use `make merge`. --- @@ -169,7 +168,7 @@ Return to the coding loop. When fixed, run `/ship` again from Step 1. ### After merge — QA label and success report -Apply the QA label only in **two-branch mode** (`` is set AND `` is empty): +Apply the QA label only in **two-branch mode** (`dev` is set AND `` is empty): - If `` is non-empty AND a linked issue number was identified in Step 3: ``` gh issue edit --add-label "" @@ -180,7 +179,7 @@ Apply the QA label only in **two-branch mode** (`` is set AND `` is empty): Report success based on mode: - **Trunk mode**: "PR merged. Issue #N closed automatically. Run `make deploy-prod` when ready to deploy to production." - **Two-branch mode**: "PR merged. Issues stay open until testing confirms the fix. Run `make deploy-preview` when ready to deploy to preview." -- **Three-branch mode**: "PR merged to ``. Promote to `` when ready for staging." +- **Three-branch mode**: "PR merged to `dev`. Promote to `` when ready for staging." --- @@ -190,5 +189,6 @@ Report success based on mode: - When `ai` is `"ai"`, never merge if the review verdict is REQUEST CHANGES. - When `ai` is `"advisory"`, always merge after review completes, regardless of verdict. - When `ai` is `"off"`, skip the review agent entirely — merge immediately after checks pass. -- In trunk mode, merges target `main`. Otherwise, `/ship` merges only to `` — never directly to `` or `main`. +- In trunk mode, merges target `main`. Otherwise, `/ship` merges only to `dev` — never directly to `` or `main`. - If `make merge` fails for any reason, report it and stop — do not attempt workarounds. + diff --git a/.claude/commands/start.md b/.claude/commands/start.md index cb645ae..cdac11e 100644 --- a/.claude/commands/start.md +++ b/.claude/commands/start.md @@ -1,4 +1,3 @@ - You are executing the `/start` skill. Your argument is: $ARGUMENTS --- @@ -209,3 +208,4 @@ When done, say: **"The code is ready for review. Please run `make dev` and test - `gh issue create` must use `--title` and `--body` flags. Never open an interactive editor. - The issue is assigned to `@me` at creation. If you are creating a ticket on someone else's behalf, remove the assignee after creation with `gh issue edit --remove-assignee @me`. - Apply resolved labels and milestone to every new issue. Label resolution order: per-invocation flag → pool selection from `` → omit (or create if `false` is `true`). Never apply a label not in `` unless `false` is `true`. Milestone resolution order: per-invocation flag → `` config → auto-detected from GitHub open milestones. Never prompt for a milestone more than once per invocation. + diff --git a/.claude/commands/status.md b/.claude/commands/status.md index acf4a70..65c177c 100644 --- a/.claude/commands/status.md +++ b/.claude/commands/status.md @@ -1,4 +1,3 @@ - You are executing the `/status` skill. Your argument is: $ARGUMENTS --- @@ -176,3 +175,4 @@ Do not post, comment, write files, or take any action. Output only. - If `gh` is unauthenticated or any fetch fails, report the error and stop immediately. - Do not retry failed commands. - Strip the leading `@` from the subject when passing to `gh` flags that do not accept it. + diff --git a/.claude/commands/version.md b/.claude/commands/version.md index cbb3322..8fa03d9 100644 --- a/.claude/commands/version.md +++ b/.claude/commands/version.md @@ -1,4 +1,3 @@ - You are executing the `/version` skill. Your argument is: $ARGUMENTS --- @@ -12,7 +11,7 @@ git branch --show-current Determine the required branch: - If `` is non-empty → required branch is `` (three-branch mode). -- Else if `` is non-empty → required branch is `` (two-branch mode). +- Else if `dev` is non-empty → required branch is `dev` (two-branch mode). - Else → required branch is `main` (trunk mode). If not on the required branch, abort and say: "Switch to `` before running `/version`." @@ -72,6 +71,7 @@ Both the version bump commit and the tag must be pushed. Tell the user the new version and tag: Report based on mode: -- **Trunk mode** (`` empty): "Tagged vX.Y.Z. Run `/release` to create the GitHub Release." -- **Two-branch mode** (`` set, `` empty): "Tagged vX.Y.Z. Run `make deploy-preview` to deploy to preview for testing. When testing is complete, run `/release`." +- **Trunk mode** (`dev` empty): "Tagged vX.Y.Z. Run `/release` to create the GitHub Release." +- **Two-branch mode** (`dev` set, `` empty): "Tagged vX.Y.Z. Run `make deploy-preview` to deploy to preview for testing. When testing is complete, run `/release`." - **Three-branch mode** (both set): "Tagged vX.Y.Z. Run `make deploy-preview` to deploy to staging. When testing is complete, run `/release`." + diff --git a/.claude/review-agent-prompt.md b/.claude/review-agent-prompt.md index 218527d..85601f5 100644 --- a/.claude/review-agent-prompt.md +++ b/.claude/review-agent-prompt.md @@ -1,4 +1,3 @@ - # Code Review Agent Prompt You are a code review agent for this project. You review feature branch pull requests before they are merged. @@ -106,3 +105,4 @@ If no findings: "No issues found. Code looks correct and follows project convent - Do not suggest improvements outside the scope of the PR. - Do not re-review files that haven't changed. - Your review is posted to GitHub for the audit trail — keep it professional. + diff --git a/sync.sh b/sync.sh index 252c351..67c30ff 100755 --- a/sync.sh +++ b/sync.sh @@ -118,10 +118,11 @@ def read_file_info(file_path): """ Inspect an existing file for Code Cannon provenance (current or legacy sync marker). - Returns (stored_hash, body_hash, is_generated) where: + Returns (stored_hash, body_hash, is_generated, needs_migration) where: stored_hash — hash recorded in the marker line when the file was last written - body_hash — hash of the file's actual content (after the marker line) + body_hash — hash of the file's actual content (excluding the marker line) is_generated — True if the file was written by this sync tool (current or legacy marker) + needs_migration — True if the marker is in legacy position (start) and should be moved to end Comparing stored_hash vs body_hash tells us whether the file was edited after the last sync. Comparing body_hash vs the freshly-computed h tells @@ -129,22 +130,39 @@ def read_file_info(file_path): """ path = Path(file_path) if not path.exists(): - return None, None, False + return None, None, False, False content = path.read_text() - parts = content.split('\n', 1) - first_line = parts[0] - - if not first_line_has_sync_marker(first_line): - return None, None, False - - match = re.search(r'hash: ([a-f0-9]+)', first_line) + lines = content.rstrip('\n').split('\n') + + # Marker can be on the last line (current) or the first line (legacy). + marker_line = None + marker_pos = None + if lines and first_line_has_sync_marker(lines[-1]): + marker_line = lines[-1] + marker_pos = 'end' + elif lines and first_line_has_sync_marker(lines[0]): + marker_line = lines[0] + marker_pos = 'start' + + if marker_line is None: + return None, None, False, False + + match = re.search(r'hash: ([a-f0-9]+)', marker_line) stored_hash = match.group(1) if match else None - body = parts[1] if len(parts) > 1 else '' + if marker_pos == 'end': + body = '\n'.join(lines[:-1]) + '\n' + else: + # Legacy position (start) — extract body after marker line. + # The stored_hash was computed over this same body, so an unedited + # legacy file will pass the customization guard (body_hash == stored_hash) + # and then fail the up-to-date check (body_hash != h) because h is + # computed from the new full_content, triggering regeneration. + body = '\n'.join(lines[1:]) + '\n' body_hash = content_hash(body) - return stored_hash, body_hash, True + return stored_hash, body_hash, True, (marker_pos == 'start') # ── Adapter loading ─────────────────────────────────────────────────────────── @@ -213,18 +231,19 @@ def sync_skill(skill_path, adapter, project_config, project_root, args): # Compute hash (of content, before marker line) h = content_hash(full_content) - marker_line = f"\n" - final_content = marker_line + full_content + marker_line = f"" + final_content = full_content + marker_line + '\n' # Check existing file - stored_hash, body_hash, is_generated = read_file_info(out_path) + stored_hash, body_hash, is_generated, needs_migration = read_file_info(out_path) skill_type = fm.get('type', 'skill') type_tag = f" [{skill_type}]" if skill_type != 'skill' else '' - # body_hash is computed over the file content after the marker line, + # body_hash is computed over the file content excluding the marker line, # which is exactly what we'd write as full_content. If they match, we're done. - if body_hash == h: + # Exception: if the marker is in legacy position, we must rewrite to migrate it. + if body_hash == h and not needs_migration: print(f" ✓ {skill_name}{type_tag} (up to date)") return False