From 13ce5e0da5b3628b256a2d3e8d33fc66a99bf78f Mon Sep 17 00:00:00 2001 From: Sebastien Taggart Date: Wed, 5 Aug 2026 11:30:16 -0400 Subject: [PATCH 1/9] Apply schema-declared placeholder defaults generically in sync.py --- sync.py | 73 ++++++++++++++++++++++++++++++++-- templates/codecannon.yaml | 5 +++ tests/test_sync.py | 82 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 156 insertions(+), 4 deletions(-) diff --git a/sync.py b/sync.py index 75a0f94..794ac2a 100755 --- a/sync.py +++ b/sync.py @@ -139,6 +139,68 @@ def close_block(): return result +def load_schema_defaults(schema_path): + """Parse config.schema.yaml's `placeholders:` section into {NAME: default}. + + Deliberately a dedicated parser rather than an extension of parse_yaml_simple: + the schema nests three levels deep (placeholders: -> NAME: -> default:), one + level deeper than parse_yaml_simple supports, and that parser is reused by + project .codecannon.yaml loading where the flatter shape is intentional. + Handles simple quoted values and literal block scalars (`default: |`). + """ + defaults = {} + lines = schema_path.read_text().splitlines() + in_placeholders = False + current_key = None + i = 0 + while i < len(lines): + line = lines[i] + stripped = line.strip() + if not stripped or stripped.startswith('#'): + i += 1 + continue + indent = len(line) - len(line.lstrip()) + + if indent == 0: + in_placeholders = stripped.rstrip(':') == 'placeholders' + current_key = None + i += 1 + continue + + if not in_placeholders: + i += 1 + continue + + if indent == 2 and stripped.endswith(':'): + current_key = stripped[:-1] + i += 1 + continue + + if indent == 4 and stripped.startswith('default:') and current_key: + value = stripped[len('default:'):].strip() + if value in ('|', '|-', '|+'): + block_lines = [] + i += 1 + while i < len(lines) and (not lines[i].strip() or (len(lines[i]) - len(lines[i].lstrip())) > 4): + block_lines.append(lines[i]) + i += 1 + content_indent = min( + (len(l) - len(l.lstrip()) for l in block_lines if l.strip()), default=0) + block_value = '\n'.join( + l[content_indent:] if l.strip() else '' for l in block_lines).rstrip('\n') + if block_value and value != '|-': + block_value += '\n' + defaults[current_key] = block_value + continue + defaults[current_key] = _dequote(value) + i += 1 + continue + + i += 1 + + return defaults + + def parse_frontmatter(text): """Extract YAML frontmatter between --- delimiters. Returns (fm_dict, body_str).""" match = re.match(r'^---\n(.*?)\n---\n(.*)', text, re.DOTALL) @@ -628,9 +690,14 @@ def main(): project_config = raw_config.get('config', {}) skill_group = raw_config.get('skill_group', '') - # Default for optional placeholders that the template ships commented out - # but skills reference unconditionally. Matches the documented default. - project_config.setdefault('TICKET_LABEL_CREATION_ALLOWED', 'false') + # Apply schema-declared defaults for any placeholder the project config + # omits (e.g. optional settings the template ships commented out but + # skills reference unconditionally). config.schema.yaml is the single + # source of truth for these — never hardcode a default here. + schema_path = CODECANNON_DIR / 'config.schema.yaml' + if schema_path.exists(): + for key, default_value in load_schema_defaults(schema_path).items(): + project_config.setdefault(key, default_value) if not adapters_list: print("Error: no adapters specified in config. Add 'adapters: [claude]' to .codecannon.yaml") diff --git a/templates/codecannon.yaml b/templates/codecannon.yaml index ac56e7b..eec4201 100644 --- a/templates/codecannon.yaml +++ b/templates/codecannon.yaml @@ -119,6 +119,11 @@ config: # Leave empty to skip label application on fail. # QA_FAILED_LABEL: "qa-failed" + # ── Status skill ─────────────────────────────────────────────────────────── + # Days after which an open PR or issue is flagged stale in /status output. + # Set to 0 to disable stale detection entirely. + # STALE_DAYS: "14" + # ── Review agent conventions ────────────────────────────────────────────────── # These values are injected into the review agent prompt at sync time. # Use YAML block scalars (|) for multi-line content. HTML comments are invisible diff --git a/tests/test_sync.py b/tests/test_sync.py index 89005f8..bca0c79 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -3,6 +3,7 @@ import hashlib import json import os +import re import subprocess import sys import tempfile @@ -175,6 +176,65 @@ def test_block_scalar_at_eof(self): self.assertEqual(result["config"]["NOTES"], "last line\n") +class TestLoadSchemaDefaults(unittest.TestCase): + """Tests for load_schema_defaults(), which reads placeholder defaults + out of config.schema.yaml so main() never has to hardcode them. + """ + + def test_simple_and_block_scalar_defaults(self): + schema_text = ( + "top_level:\n" + "\n" + " skill_group:\n" + " description: not a placeholder default\n" + " default: should-not-appear\n" + "\n" + "placeholders:\n" + "\n" + " BRANCH_PROD:\n" + " description: prod branch\n" + " default: \"main\"\n" + " category: branches\n" + "\n" + " EMPTY_DEFAULT:\n" + " description: intentionally blank\n" + " default: \"\"\n" + "\n" + " MULTI_LINE:\n" + " description: a list\n" + " default: |\n" + " - one\n" + " - two\n" + " category: review\n" + ) + with tempfile.TemporaryDirectory() as tmpdir: + schema_path = Path(tmpdir) / "config.schema.yaml" + schema_path.write_text(schema_text) + result = sync.load_schema_defaults(schema_path) + + self.assertEqual(result["BRANCH_PROD"], "main") + self.assertEqual(result["EMPTY_DEFAULT"], "") + self.assertEqual(result["MULTI_LINE"], "- one\n- two\n") + self.assertNotIn("skill_group", result) + + def test_real_schema_defines_stale_days(self): + """Regression test for #194: STALE_DAYS must have a schema default.""" + result = sync.load_schema_defaults(REPO_ROOT / "config.schema.yaml") + self.assertEqual(result.get("STALE_DAYS"), "14") + + def test_real_schema_all_placeholders_have_defaults(self): + """Every entry under placeholders: in the real schema should parse a default.""" + result = sync.load_schema_defaults(REPO_ROOT / "config.schema.yaml") + schema_text = (REPO_ROOT / "config.schema.yaml").read_text() + # Only count keys under `placeholders:` (after the top_level: block ends) + placeholders_start = schema_text.index("placeholders:") + placeholder_names = re.findall( + r"^ ([A-Z_]+):$", schema_text[placeholders_start:], re.MULTILINE) + self.assertTrue(placeholder_names, "fixture sanity check: schema should list placeholders") + for name in placeholder_names: + self.assertIn(name, result, f"{name} has no parsed default") + + class TestParseFrontmatter(unittest.TestCase): def test_basic_frontmatter(self): @@ -909,6 +969,22 @@ def test_missing_skill_group_exits_1(self): sync.main() self.assertEqual(ctx.exception.code, 1) + def test_validate_passes_when_optional_placeholder_omitted(self): + """A project config that omits an optional, schema-defaulted placeholder + (STALE_DAYS) should still validate — main() must backfill it from + config.schema.yaml rather than requiring it to be spelled out. + Regression test for #194. + """ + self._chdir_to_project() + real_config = (REPO_ROOT / ".codecannon.yaml").read_text() + lines = [l for l in real_config.splitlines() if "STALE_DAYS" not in l] + self.assertNotIn("STALE_DAYS", "\n".join(lines), "fixture setup: STALE_DAYS should be stripped") + with tempfile.TemporaryDirectory() as tmpdir: + cfg = Path(tmpdir) / "no-stale-days.yaml" + cfg.write_text("\n".join(lines) + "\n") + with patch("sys.argv", ["sync.py", "--config", str(cfg), "--validate"]): + sync.main() # should not raise SystemExit(1) + def test_nonexistent_skill_group_exits_1(self): """skill_group naming a directory that doesn't exist should fail loudly.""" with tempfile.TemporaryDirectory() as tmpdir: @@ -949,7 +1025,11 @@ def test_all_generated_files_are_current(self): raw_config = sync.parse_yaml_simple(config_path.read_text()) adapters_list = raw_config.get("adapters", []) project_config = raw_config.get("config", {}) - project_config.setdefault("TICKET_LABEL_CREATION_ALLOWED", "false") + # Mirrors main()'s schema-default application, not a hardcoded special case. + schema_path = REPO_ROOT / "config.schema.yaml" + if schema_path.exists(): + for key, default_value in sync.load_schema_defaults(schema_path).items(): + project_config.setdefault(key, default_value) skill_group = raw_config.get("skill_group", "") if not skill_group: self.skipTest("skill_group not set in .codecannon.yaml") From b66e73de1d7e1d8ccc758c670d7183aa7d8cac61 Mon Sep 17 00:00:00 2001 From: Sebastien Taggart Date: Wed, 5 Aug 2026 11:52:37 -0400 Subject: [PATCH 2/9] Fix QA-label opt-out regression and inline-comment parsing in schema defaults --- config.schema.yaml | 12 +++++------ sync.py | 47 ++++++++++++++++++++++++++++++++-------- tests/test_sync.py | 54 +++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 93 insertions(+), 20 deletions(-) diff --git a/config.schema.yaml b/config.schema.yaml index 83d1817..b5d148b 100644 --- a/config.schema.yaml +++ b/config.schema.yaml @@ -193,20 +193,20 @@ placeholders: used_in: [start] QA_READY_LABEL: - description: "Label applied by /submit-for-review when a feature merges to BRANCH_DEV in two-branch mode, signaling it is ready for QA on the preview environment. In trunk and three-branch modes, /submit-for-review does not apply this label automatically. Only configure if you have an explicit QA gate for your integration branch environment." - default: "ready-for-qa" + description: "Label applied by /submit-for-review when a feature merges to BRANCH_DEV in two-branch mode, signaling it is ready for QA on the preview environment. In trunk and three-branch modes, /submit-for-review does not apply this label automatically. Only configure if you have an explicit QA gate for your integration branch environment. Empty (the default) disables the QA label workflow entirely — this is an opt-in feature, not merely an unset value, so its default must stay empty even though 'ready-for-qa' is the recommended label name once you do configure it." + default: "" category: github used_in: [submit-for-review, qa] QA_PASSED_LABEL: - description: "Label applied by /qa when a feature passes QA review. Leave empty to skip label application." - default: "qa-passed" + description: "Label applied by /qa when a feature passes QA review. Leave empty to skip label application. Empty (the default) is intentional — see QA_READY_LABEL." + default: "" category: github used_in: [qa] QA_FAILED_LABEL: - description: "Label applied by /qa when a feature fails QA review. Leave empty to skip label application." - default: "qa-failed" + description: "Label applied by /qa when a feature fails QA review. Leave empty to skip label application. Empty (the default) is intentional — see QA_READY_LABEL." + default: "" category: github used_in: [qa] diff --git a/sync.py b/sync.py index 794ac2a..2e23da9 100755 --- a/sync.py +++ b/sync.py @@ -139,6 +139,23 @@ def close_block(): return result +def _strip_inline_comment(s): + """Strip a trailing ` #...` comment from a single-line YAML scalar, honoring quotes. + + Only a `#` outside any quoted span, preceded by whitespace or at position 0, + starts a comment — matches the convention used elsewhere for full-line comments. + """ + in_squote = in_dquote = False + for idx, ch in enumerate(s): + if ch == '"' and not in_squote: + in_dquote = not in_dquote + elif ch == "'" and not in_dquote: + in_squote = not in_squote + elif ch == '#' and not in_squote and not in_dquote and (idx == 0 or s[idx - 1].isspace()): + return s[:idx].rstrip() + return s + + def load_schema_defaults(schema_path): """Parse config.schema.yaml's `placeholders:` section into {NAME: default}. @@ -171,13 +188,16 @@ def load_schema_defaults(schema_path): i += 1 continue - if indent == 2 and stripped.endswith(':'): - current_key = stripped[:-1] - i += 1 - continue + if indent == 2 and ':' in stripped: + key_part, _, rest = stripped.partition(':') + rest = rest.strip() + if rest == '' or rest.startswith('#'): + current_key = key_part.strip() + i += 1 + continue if indent == 4 and stripped.startswith('default:') and current_key: - value = stripped[len('default:'):].strip() + value = _strip_inline_comment(stripped[len('default:'):].strip()) if value in ('|', '|-', '|+'): block_lines = [] i += 1 @@ -201,6 +221,18 @@ def load_schema_defaults(schema_path): return defaults +def apply_schema_defaults(project_config, schema_path): + """Backfill project_config with config.schema.yaml's declared defaults, in place. + + Used by both main() and the golden-file snapshot test so the two never diverge + on what "the real sync behavior" applies. No-op if schema_path doesn't exist. + """ + if not schema_path.exists(): + return + for key, default_value in load_schema_defaults(schema_path).items(): + project_config.setdefault(key, default_value) + + def parse_frontmatter(text): """Extract YAML frontmatter between --- delimiters. Returns (fm_dict, body_str).""" match = re.match(r'^---\n(.*?)\n---\n(.*)', text, re.DOTALL) @@ -694,10 +726,7 @@ def main(): # omits (e.g. optional settings the template ships commented out but # skills reference unconditionally). config.schema.yaml is the single # source of truth for these — never hardcode a default here. - schema_path = CODECANNON_DIR / 'config.schema.yaml' - if schema_path.exists(): - for key, default_value in load_schema_defaults(schema_path).items(): - project_config.setdefault(key, default_value) + apply_schema_defaults(project_config, CODECANNON_DIR / 'config.schema.yaml') if not adapters_list: print("Error: no adapters specified in config. Add 'adapters: [claude]' to .codecannon.yaml") diff --git a/tests/test_sync.py b/tests/test_sync.py index bca0c79..8a3c585 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -217,11 +217,58 @@ def test_simple_and_block_scalar_defaults(self): self.assertEqual(result["MULTI_LINE"], "- one\n- two\n") self.assertNotIn("skill_group", result) + def test_default_with_inline_comment(self): + """A trailing ` # comment` after a quoted default must not corrupt the value. + Regression test: previously `default: "main" # note` parsed to the literal + string with quotes and comment still attached, since the comment was never + stripped before _dequote() (which only strips quotes at the exact string ends). + """ + schema_text = ( + "placeholders:\n" + "\n" + " BRANCH_PROD:\n" + " default: \"main\" # matches templates/codecannon.yaml\n" + ) + with tempfile.TemporaryDirectory() as tmpdir: + schema_path = Path(tmpdir) / "config.schema.yaml" + schema_path.write_text(schema_text) + result = sync.load_schema_defaults(schema_path) + self.assertEqual(result["BRANCH_PROD"], "main") + + def test_key_line_with_inline_comment(self): + """A trailing ` # comment` on a placeholder key line must not hide the key. + Regression test: previously `FOO: # note` failed the strict `endswith(':')` + check, so current_key was never set and the following default: line — guarded + on `current_key` — was silently skipped, dropping the default entirely. + """ + schema_text = ( + "placeholders:\n" + "\n" + " FOO: # a note\n" + " default: \"bar\"\n" + ) + with tempfile.TemporaryDirectory() as tmpdir: + schema_path = Path(tmpdir) / "config.schema.yaml" + schema_path.write_text(schema_text) + result = sync.load_schema_defaults(schema_path) + self.assertEqual(result.get("FOO"), "bar") + def test_real_schema_defines_stale_days(self): """Regression test for #194: STALE_DAYS must have a schema default.""" result = sync.load_schema_defaults(REPO_ROOT / "config.schema.yaml") self.assertEqual(result.get("STALE_DAYS"), "14") + def test_qa_labels_default_empty_to_preserve_opt_out(self): + """QA_READY_LABEL/QA_PASSED_LABEL/QA_FAILED_LABEL must default to "" — they + gate {{#if}} sections in qa.md / submit-for-review.md that templates/codecannon.yaml + documents as "leave empty to disable". A non-empty schema default would flip + those sections on for every project that never configured QA labeling. + """ + result = sync.load_schema_defaults(REPO_ROOT / "config.schema.yaml") + self.assertEqual(result.get("QA_READY_LABEL"), "") + self.assertEqual(result.get("QA_PASSED_LABEL"), "") + self.assertEqual(result.get("QA_FAILED_LABEL"), "") + def test_real_schema_all_placeholders_have_defaults(self): """Every entry under placeholders: in the real schema should parse a default.""" result = sync.load_schema_defaults(REPO_ROOT / "config.schema.yaml") @@ -1025,11 +1072,8 @@ def test_all_generated_files_are_current(self): raw_config = sync.parse_yaml_simple(config_path.read_text()) adapters_list = raw_config.get("adapters", []) project_config = raw_config.get("config", {}) - # Mirrors main()'s schema-default application, not a hardcoded special case. - schema_path = REPO_ROOT / "config.schema.yaml" - if schema_path.exists(): - for key, default_value in sync.load_schema_defaults(schema_path).items(): - project_config.setdefault(key, default_value) + # Same helper main() uses, so this test can't silently drift from real behavior. + sync.apply_schema_defaults(project_config, REPO_ROOT / "config.schema.yaml") skill_group = raw_config.get("skill_group", "") if not skill_group: self.skipTest("skill_group not set in .codecannon.yaml") From b4de127397b2fd28296de4bca84c4333696147cb Mon Sep 17 00:00:00 2001 From: Sebastien Taggart Date: Wed, 5 Aug 2026 12:43:55 -0400 Subject: [PATCH 3/9] Pull back procedural over-specification in skills; add TEST_CMD and skill-design philosophy --- .agents/skills/deploy/SKILL.md | 195 ++++----------- .agents/skills/start/SKILL.md | 38 +-- .agents/skills/status/SKILL.md | 338 +++++--------------------- .claude/commands/deploy.md | 195 ++++----------- .claude/commands/start.md | 38 +-- .claude/commands/status.md | 338 +++++--------------------- .codecannon.yaml | 1 + .cursor/rules/deploy.mdc | 195 ++++----------- .cursor/rules/start.mdc | 38 +-- .cursor/rules/status.mdc | 338 +++++--------------------- .gemini/skills/deploy/SKILL.md | 195 ++++----------- .gemini/skills/start/SKILL.md | 38 +-- .gemini/skills/status/SKILL.md | 338 +++++--------------------- AGENTS.md | 20 ++ README.md | 12 + config.schema.yaml | 6 + docs/config-reference.md | 1 + skills/github-agile/deploy.md | 429 +++++---------------------------- skills/github-agile/start.md | 67 +++-- skills/github-agile/status.md | 336 +++++--------------------- templates/codecannon.yaml | 5 + tests/test_sync.py | 74 ++++++ 22 files changed, 783 insertions(+), 2452 deletions(-) diff --git a/.agents/skills/deploy/SKILL.md b/.agents/skills/deploy/SKILL.md index c7cdad4..d10a6e1 100644 --- a/.agents/skills/deploy/SKILL.md +++ b/.agents/skills/deploy/SKILL.md @@ -9,25 +9,24 @@ description: Code Cannon: Bump the project version, create a GitHub Release, and ## What `/deploy` does -`/deploy` is the final step in the workflow. It combines version bumping and release creation into a single command: check state, optionally bump the version, then create a GitHub Release (and in multi-branch mode, promote to production). +`/deploy` is the final step in the workflow. It combines version bumping and release creation into a single command: check state, optionally bump the version, then create a GitHub Release (and in multi-branch mode, promote the deploy branch to production first). + +The branching mode changes the shape of the release: in **trunk mode** (`BRANCH_PROD` only) `/deploy` tags and releases the current branch directly; in **multi-branch mode** (`BRANCH_DEV` set, optionally with `BRANCH_TEST`) it first opens and merges a release PR from the deploy branch into production, and that merge is what closes the linked issues. --- -## Step 1 — Verify branch +## Step 1 — Verify branch and sync -Run: -```bash -git branch --show-current -``` +Run `git branch --show-current`. The **deploy branch** for this project is: -Required branch: `dev` (two-branch mode). +`dev` (two-branch mode). -If not on the required branch, abort and say: "Switch to `` before running `/deploy`." +If not on the deploy branch, abort: "Switch to `` before running `/deploy`." -Sync to the remote before proceeding. The script below guards against uncommitted local changes, then runs `git checkout`, `git fetch`, and `git reset --hard origin/` as one atomic operation. The deploy branch is never edited locally under the CodeCannon workflow (only fast-forwarded from CodeCannon's own merges), so the hard reset is the correct sync; the dirty-tree guard catches accidental local edits before they get silently discarded. +Then sync it to the remote. The script guards against uncommitted local changes, then runs `git checkout`, `git fetch`, and `git reset --hard origin/` as one atomic operation. The deploy branch is never edited locally under the CodeCannon workflow (only fast-forwarded from merges), so the hard reset is the correct sync; the dirty-tree guard catches accidental local edits before they are silently discarded. ```bash -python3 CodeCannon/skills/github-agile/scripts/sync-base-branch.py dev +python3 CodeCannon/skills/github-agile/scripts/sync-base-branch.py ``` If the script exits non-zero, stop and resolve the issue it reports before continuing. @@ -36,87 +35,35 @@ If the script exits non-zero, stop and resolve the issue it reports before conti ## Step 2 — Check current state -### Find the latest version tag - -```bash -git describe --tags --abbrev=0 -``` +Find the latest version tag (`git describe --tags --abbrev=0`; if none, note this is the first release) and read the current version with `cat VERSION`. -If no tag exists, note this is the first release. - -### Read current version +Show the merge commits (and their PRs) since the last tag. The range depends on the mode: ```bash -cat VERSION +git log main.. --merges --pretty=format:"%s" ``` -### Show commits since last tag +Merge-commit subjects have the form `Merge pull request #N from branch/name` — parse the PR numbers, then retrieve each body with `gh pr view --json number,title,body`. -If a previous tag exists, show what's on the branch since that tag: +From those PR bodies, compile the release's issue links: -```bash -git log main..dev --merges --pretty=format:"%s" -``` - -Parse PR numbers from merge commit subjects (format: `Merge pull request #N from branch/name`). +Keep closing keywords and context references **separate — do not merge them into one set**: -For each PR number found, retrieve the PR body: -```bash -gh pr view --json number,title,body -``` +- **Close set** — the union of every `Closes #N` line across all constituent PR bodies. These auto-close when the release PR merges into `main`. Record, per constituent PR, the exact `Closes #N` lines so they can be reproduced verbatim in the release PR body. +- **Reference set** — issues mentioned only via `Related to #N` or the legacy `Issue #N` form. These are context links and will **not** close. Legacy `Issue #N` carries no recoverable close-intent, so it stays in the reference set rather than being guessed into the close set; the human gate surfaces it so you can manually close any straggler that should have closed. +- **PRs included** (number + title). -Extract closing keywords **separately** from context references — do **not** merge them into a single set: +Also check for open unmerged PRs (`gh pr list --state open --json number,title,headRefName`). -- **Close set** — the union of every `Closes #N` line across all constituent PR bodies. These issues will auto-close when the release PR merges into `main`. Record, per constituent PR, the exact `Closes #N` lines it contained so they can be reproduced verbatim in the release PR body. -- **Reference set** — issues mentioned only via `Related to #N`, or via the legacy `Issue #N` form. These are context links and will **not** close. Legacy `Issue #N` carries no recoverable close-intent, so it stays in the reference set rather than being guessed into the close set; the HUMAN GATE surfaces it so you can manually close any straggler that should have been a `Closes`. - -Compile: -- List of PRs included (number + title) -- Close set and reference set, kept distinct - -### Check for open unmerged PRs - -```bash -gh pr list --state open --json number,title,headRefName -``` - -### Present the summary - -Tell the user: - -``` -Current version: X.Y.Z -Latest tag: vX.Y.Z - -Commits/PRs since last tag: - #17 — Add /docs directory - #18 — Fix checkout runtime error - -Open PRs not yet merged: - #19 — Add dark mode (feature/dark-mode) - -Would you like to bump the version before deploying? - - **patch** → X.Y.C - - **minor** → X.B.0 - - **major** → A.0.0 - - **specific** → enter a version number - - **skip** → proceed to release with the latest existing tag -``` - -Wait for their response. +Present a summary — current version, latest tag, the PRs/issues since that tag, any open PRs — and ask whether to bump the version before deploying (patch → X.Y.C, minor → X.B.0, major → A.0.0, a specific version, or skip to release the latest existing tag). Wait for their response. --- ## Step 3 — Version bump (if requested) -If the user chose to skip, find the latest version tag in the branch history: -```bash -git describe --tags --abbrev=0 -``` - -If no tag is found at all (first release), warn: "No version tag found. You must bump the version before deploying." Return to the version bump prompt. Otherwise, use the tag found as the release version. +If the user chose **skip**, use the latest existing tag (`git describe --tags --abbrev=0`) as the release version. If none exists (first release), warn "No version tag found. You must bump the version before deploying." and return to the bump prompt. -If the user chose a bump level, map their response to a bump command and run `bump-and-tag.py`, which performs the bump, verifies the resulting tag (creating an annotated fallback if `tag.forceSignAnnotated` silently rejected a lightweight tag), and pushes both the commit and the tag. The resolved version is printed on stdout — capture it for the release step. +If the user chose a bump level, map it to a bump command and run `bump-and-tag.py`, which performs the bump, verifies the resulting tag (creating an annotated fallback if `tag.forceSignAnnotated` silently rejected a lightweight tag), and pushes both the commit and the tag. The resolved version is printed on stdout — capture it as ``. | User says | `--bump-cmd` | |---|---| @@ -131,59 +78,37 @@ python3 CodeCannon/skills/github-agile/scripts/bump-and-tag.py \ --version-read-cmd "cat VERSION" ``` -If the script exits non-zero, stop and resolve the issue it reports before continuing. On success, the version printed on stdout is the new version — use it as `` in subsequent steps. +If the script exits non-zero, stop and resolve the issue it reports before continuing. --- ## Step 4 — Compute release contents -Determine the version tag (either from the bump just performed, or from the existing HEAD tag if the user skipped bumping). +Determine the release version tag (from the bump just performed, or the existing HEAD tag if the user skipped). Find the previous tag for the changelog range: `git describe --abbrev=0 ^`. -Find the previous tag to determine the range: -```bash -git describe --abbrev=0 ^ -``` - -Use the PR list, close set, and reference set already computed in Step 2. If the version bump added new commits, re-fetch if needed. +Reuse the PR list, close set, and reference set already computed in Step 2. If the version bump added new commits, re-fetch as needed. --- ## Step 5 — HUMAN GATE -Show the user the release summary. Example format: - -``` -Ready to release vX.Y.Z to production. +Show the release summary — the target version, the PRs included, and the issue links: -PRs included: - #17 — Add /docs directory - #18 — Fix checkout runtime error +- Issues that will **close** on merge (the close set, reproduced verbatim from constituent PRs). +- Issues **referenced but not closing** (the reference set — confirm none of these should actually close). -Issues that will close on merge (Closes #N, reproduced verbatim from constituent PRs): - #14 — Add /docs directory - #15 — Fix checkout runtime error - -Issues referenced but NOT closing (Related to #N / legacy Issue #N — confirm none of these should actually close): - #20 — Tighten error copy on the upload form - -Have you tested all of the above on preview? Type 'release' to confirm. -``` +Confirm the deploy branch has been tested: +"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. +Wait for "release" or an explicit confirmation. Any other response → stop and ask what they'd like to change. --- -## Step 6 — Create PR: `dev` → `main` +## Step 6 — Promote: `` → `main` -First, create a temp directory for this invocation: +Create a temp directory for this invocation (`python3 CodeCannon/skills/github-agile/scripts/make-workdir.py`) and note the returned path — use it for all temp files here. -```bash -python3 CodeCannon/skills/github-agile/scripts/make-workdir.py -``` - -Note the returned path (e.g. `/tmp/CodeCannon/a8f3b2`). Use this path for all temp files in this invocation. - -Then use your file-writing tool (not Bash) to create `/release_pr_body.md`: +Use your file-writing tool (not Bash) to create `/release_pr_body.md`: ```markdown Release vX.Y.Z @@ -198,53 +123,31 @@ Closes #15 Related to #20 ``` -Reproduce **every** `Closes #N` line from the close set computed in Step 2 — verbatim, one per line, omitting none. Add a `Related to #N` line for each issue in the reference set so the links appear on the release PR without triggering an auto-close. If the reference set is empty, omit the `Related to` lines entirely. +Reproduce **every** `Closes #N` line from the close set — verbatim, one per line, omitting none. Add a `Related to #N` line for each issue in the reference set so the links appear without triggering an auto-close; if the reference set is empty, omit the `Related to` lines entirely. -Then create the PR (do NOT use `--body`, `--body-file -`, or heredocs): +Create the PR (do NOT use `--body`, `--body-file -`, or heredocs), with `--head` set to the deploy branch: ```bash -gh pr create --base main --head dev \ +gh pr create --base main --head \ --title "Release vX.Y.Z" \ --body-file /release_pr_body.md ``` -Note the PR number from the output. - -The `Closes #N` lines will auto-close the linked issues because this PR merges into `main` (the default branch). - -> **Critical:** Use the unqualified `#N` form only. Never write `Closes owner/repo#N`, even for same-repo refs — GitHub's closing-keyword parser only populates `closingIssuesReferences` for the unqualified form, and the qualified form silently breaks auto-close. - ---- - -## Step 7 — Merge +> **Critical:** Use the unqualified `#N` form only. Never write `Closes owner/repo#N`, even for same-repo refs — GitHub's closing-keyword parser only populates `closingIssuesReferences` for the unqualified form, and the qualified form silently breaks auto-close. The `Closes #N` lines auto-close the linked issues because this PR merges into `main` (the default branch). -Do NOT use `make merge` — it refuses PRs targeting `main`. Use `gh pr merge` directly: - -```bash -gh pr merge --merge -``` +Then merge. Do NOT use `make merge` — it refuses PRs targeting `main`. Use `gh pr merge --merge` directly. --- -## Step 8 — Create GitHub Release +## Step 7 — Create the GitHub Release -**Publish confirmation — required before writing the release notes or creating the Release.** The GitHub Release is a public-surface action. The single word from Step 5 authorized the promotion/merge (already done); the public publish gets its own explicit confirmation. Tell the user (substitute the actual tag, e.g. `v0.13.0`): +**Publish confirmation — required before writing the release notes or creating the Release.** The GitHub Release is a public-surface action; the confirmation from Step 5 authorized the promotion, but the public publish gets its own explicit confirmation. Tell the user (substitute the actual tag, e.g. `v0.13.0`): > Publishing GitHub Release `` — the final public step. Confirm by pasting: `publish ` Wait for the user to paste `publish ` (or an explicit version-named variant such as `ship `). Any other response → stop and ask what they'd like to change. The version-named phrase is deliberate: Claude Code's auto-mode safety classifier requires authorization that names the release before `gh release create` runs, so the generic Step 5 confirmation is not relied on for the public publish. If a harness still blocks the call after this confirmation (e.g. an older client), the user can re-confirm with `publish release` to unblock. ---- - -The version tag (from Step 3) and the PR/issue list (from Step 4) are already known. Find the previous tag to build the changelog link: - -```bash -git describe --abbrev=0 ^ -``` - -If no previous tag exists, omit the "Full changelog" line. - -Use your file-writing tool (not Bash) to create `/release_notes.md` (same temp directory from Step 6): +The version tag and PR/issue list are already known; the previous tag comes from Step 4 (if there is no previous tag, omit the "Full changelog" line). Create a temp directory if you haven't already (`python3 CodeCannon/skills/github-agile/scripts/make-workdir.py`), then use your file-writing tool (not Bash) to create `/release_notes.md`: ```markdown ## Changes @@ -255,7 +158,7 @@ Use your file-writing tool (not Bash) to create `/release_notes.md` (sam **Full changelog:** https://github.com///compare/... ``` -Then create the release (do NOT use `--notes`, `--notes-file -`, or heredocs): +Format each PR line as `- # (PR #)`; if a PR had no linked issue, use just the PR title. Then create the release (do NOT use `--notes`, `--notes-file -`, or heredocs): ```bash gh release create \ @@ -263,15 +166,11 @@ gh release create \ --notes-file /release_notes.md ``` -Format each PR line as `- # (PR #)`. If a PR had no linked issue, omit the `#` prefix and use just the PR title. - -After the command runs, note the release URL from the output. +Note the release URL from the output. --- -## Step 9 — Report - -Tell the user: +## Step 8 — Report -> "Released vX.Y.Z. Issues #N, #M closed automatically. GitHub Release vX.Y.Z created at ``. Run `make deploy-prod` to ship to production." - +Tell the user: "Released vX.Y.Z. Linked issues are closed. GitHub Release vX.Y.Z created at ``. Run `make deploy-prod` to ship to production." + diff --git a/.agents/skills/start/SKILL.md b/.agents/skills/start/SKILL.md index ec19787..a275a43 100644 --- a/.agents/skills/start/SKILL.md +++ b/.agents/skills/start/SKILL.md @@ -27,12 +27,10 @@ Otherwise → go to **Case A: New work**. > **Execution order:** Resolve labels and milestones **now**, before entering Case A Step 1. If milestone auto-detection requires a user prompt (2+ open milestones), that prompt happens here — not later during issue creation. By the time you reach Step 2's human gate, all metadata must already be resolved so that Step 3 can proceed without re-prompting. -The argument string may contain optional inline flags after the description. Parse as follows: +The description may be followed by optional flags — `--label`/`-l` and `--milestone`/`-m`, in any order. Separate the description from the flags yourself; the flags carry these meanings: -1. **Identify flags** — scan for the first token that starts with `--label`, `-l`, `--milestone`, or `-m`. Everything before it is the **description**. Everything from the first flag onward is **flags**. -2. **`--label ` / `-l `** — comma-separated label string (e.g. `bug` or `enhancement,ux`). If provided, it **bypasses label auto-selection entirely** for this invocation — use the value verbatim. Labels containing spaces must be quoted (e.g. `--label "good first issue"`). -3. **`--milestone ` / `-m `** — milestone name or number (e.g. `Sprint 4` or `12`). Pass the value as-is; GitHub accepts both names and numbers. -4. **Flags may appear in any order** after the description. +- **`--label ` / `-l `** — a comma-separated label string used **verbatim**, bypassing label auto-selection entirely for this invocation. Quote values containing spaces (e.g. `--label "good first issue"`). +- **`--milestone ` / `-m `** — a milestone name or number (GitHub accepts both names and numbers). **Label resolution (three-tier, Case A only):** @@ -57,14 +55,6 @@ After parsing flags, determine the active milestone in this order: - **1 result** → use its title silently. Inform the user inline: `(milestone: )`. - **2+ results** → show the numbered list, ask once: **"Multiple open milestones — which should this issue go under? (enter a number or title, or 'none')"**. Accept milestone number, title, or "none"/"skip". Wait for response before continuing. -**Examples:** - -| `$ARGUMENTS` | Description | Labels | Milestone | -|---|---|---|---| -| `Add dark mode toggle to settings page` | `Add dark mode toggle to settings page` | auto-selected from pool | auto-detected | -| `Add dark mode --label enhancement` | `Add dark mode` | `enhancement` (verbatim) | auto-detected | -| `Add dark mode --label enhancement,ux --milestone "Sprint 4"` | `Add dark mode` | `enhancement,ux` (verbatim) | `Sprint 4` | - > Replace vs append: flags **replace** auto-selection entirely, they do not append. This avoids silent label duplication and milestone conflicts. --- @@ -102,7 +92,7 @@ If on any other branch → proceed to Case A or Case B as determined by the `$AR ### Step 1 — Investigate -Read the relevant code. Propose a concrete implementation approach. Be specific about which files change and how. +Read the relevant code using your harness's native file-reading and search tools (read, grep/glob, and the like) rather than shell pipelines — hand-rolled `find … | xargs`, `grep ; awk`, or redirection shapes trigger permission prompts that cannot be permanently allowed. Then propose a concrete implementation approach, specific about which files change and how. ### Step 2 — HUMAN GATE @@ -222,7 +212,14 @@ Show the user: `On branch feature/<name>` ### Step 5 — Write the code -Now write the code. Do NOT commit anything. +Write the code using your harness's native editing tools. Do NOT commit anything. +To exercise your work as you go, run the project's configured test command rather than hand-rolling a test invocation — a hand-built `python3 -m unittest … > /tmp/…` or similar redirection shape triggers permission prompts that a configured command avoids: + +```bash +make test +``` + +If it fails because the target does not exist, tell the user rather than improvising a replacement. When done, say: **"When you've verified locally, reply `yes` to submit, or say what to change."** @@ -317,7 +314,14 @@ git branch --show-current ### Step 5 — Write the code -Continue from where work left off. Do NOT commit. +Continue from where work left off, using your harness's native editing tools. Do NOT commit. +To exercise your work as you go, run the project's configured test command rather than hand-rolling a test invocation — a hand-built `python3 -m unittest … > /tmp/…` or similar redirection shape triggers permission prompts that a configured command avoids: + +```bash +make test +``` + +If it fails because the target does not exist, tell the user rather than improvising a replacement. When done, say: **"When you've verified locally, reply `yes` to submit, or say what to change."** @@ -336,4 +340,4 @@ When done, say: **"When you've verified locally, reply `yes` to submit, or say w - 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 <number> --remove-assignee @me`. - Apply resolved labels and milestone to every new issue. Label resolution order: per-invocation flag → pool selection from `bug, documentation, enhancement, chore` → omit `--label` entirely. Never apply a label outside `bug, documentation, enhancement, chore`. - Milestone resolution order: per-invocation flag → auto-detected from GitHub open milestones. Never prompt for a milestone more than once per invocation. -<!-- generated by CodeCannon/sync.py | skill: start | adapter: codex | hash: e41e83ae | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: start | adapter: codex | hash: 9c1a1a62 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.agents/skills/status/SKILL.md b/.agents/skills/status/SKILL.md index 9b7cf16..1c163bf 100644 --- a/.agents/skills/status/SKILL.md +++ b/.agents/skills/status/SKILL.md @@ -7,45 +7,39 @@ description: Code Cannon: Summarize in-progress and recently completed work from --- -## Step 1 — Parse arguments +## What `/status` does -First, check whether `$ARGUMENTS` contains `--milestone`, `--sprint`, or `--team`. +`/status` prints a read-only, standup-ready snapshot of in-progress and recently completed work, then a single "what's next" suggestion. It never writes to GitHub or the working tree — it only reads and reports. -**Milestone mode:** If `--milestone` or `--sprint` is present, extract everything after the flag as the milestone name (trim leading/trailing whitespace; preserve internal spaces). Ignore any other arguments. Enter milestone mode (Steps M1–M3 below) and skip Steps 2–6. +Because it is read-only, the *shape* of its output does not matter: a differently-formatted-but-accurate summary is a fine result. Derive a clear, scannable layout yourself. What this skill pins down is the data to fetch, how to classify it, and the one piece of real opinion — the "what's next" ordering. -Examples: -- `--milestone Sprint 4` → milestone name = `Sprint 4` -- `--sprint Sprint 4` → milestone name = `Sprint 4` -- `--milestone Q2 Release` → milestone name = `Q2 Release` -- `--milestone 12` → milestone name = `12` - -**Team mode:** If `--team` is present, enter team mode (Steps T1–T3 below) and skip Steps 2–6. `--team` is mutually exclusive with `--milestone`/`--sprint` and username arguments. If both are present, report the conflict and stop. +--- -**Personal mode** (no `--milestone` / `--sprint` / `--team` flag): determine: +## Step 1 — Determine mode -- **subject**: default `@me`. If the argument starts with `@` or is a plain word that is not a number, treat it as a GitHub username. Strip the leading `@` for `gh` commands that do not accept it (e.g. `gh pr list --author alice`); keep it for display. -- **lookback**: default `7`. If the argument is a number (digits only), use it as the lookback window in days. +Three mutually exclusive modes, selected from `$ARGUMENTS`: -No argument → subject = `@me`, lookback = `7`. +- **Milestone mode** — `--milestone` or `--sprint` is present. Everything after the flag is the milestone name (a name or a number; trim outer whitespace, preserve internal spaces). Ignore other arguments. Run Steps M1–M2. +- **Team mode** — `--team` is present. Run Steps T1–T2. `--team` is mutually exclusive with `--milestone`/`--sprint` and with a username; if combined, report the conflict and stop. +- **Personal mode** — no mode flag. **Subject** defaults to `@me`; a `@name` or a non-numeric word is a username (strip the leading `@` for `gh` flags that reject it, keep it for display). **Lookback** defaults to `7`; a bare number is the lookback in days. --- -## Step 2 — Fetch GitHub data (run all in parallel) +## Step 2 — Fetch GitHub data (personal mode) -Run these commands concurrently: +Run these concurrently. If any `gh` command exits non-zero (including auth errors), report the message and stop — do not retry. -**Open PRs authored by subject:** +**Open PRs authored by subject** — request enough fields to derive health (draft, CI, review decision, merge conflict) and staleness: ```bash gh pr list --author <subject> --state open \ --json number,title,url,labels,milestone,baseRefName,body,reviewDecision,statusCheckRollup,updatedAt,mergeable,isDraft ``` -**Recently merged PRs (last `<lookback>` days):** +**Recently merged PRs**, filtered to those merged within `<lookback>` days: ```bash gh pr list --author <subject> --state merged --limit 20 \ --json number,title,url,mergedAt,labels,baseRefName ``` -Filter the results to keep only entries where `mergedAt` is within the last `<lookback>` days. **Open issues assigned to subject:** ```bash @@ -53,317 +47,103 @@ gh issue list --assignee <subject> --state open \ --json number,title,url,labels,milestone,updatedAt ``` -**PRs requesting your review** (only when subject is `@me`): +**PRs requesting your review** — only when subject is `@me`; skip for other users: ```bash gh pr list --search "review-requested:@me" --state open \ --json number,title,url,author,updatedAt ``` -Skip this query when viewing another user's status. -If any `gh` command exits with a non-zero status (including auth errors), report the error message and stop. Do not retry. - ---- - -## Step 3 — Fetch local git context - -Check if the current directory is inside a git repository: -```bash -git rev-parse --is-inside-work-tree -``` - -If yes, run: +Also fetch local git context (skip and note if not in a git repo — `git rev-parse --is-inside-work-tree`): ```bash git log --oneline --since="<lookback> days ago" ``` -If not inside a git repo, skip this step and note it was skipped in the output. - --- -## Step 4 — Classify items - -Using the data from Steps 2 and 3, classify each item: - -- **In progress** — open PRs. For each, attempt to identify a linked issue number from the PR body (look for `#N`, `closes #N`, `fixes #N`, `issue #N`). If found, cross-reference with open issues. -- **Done** — merged PRs within the lookback window. -- **Up next** — open issues that are NOT associated with any open PR (i.e. no open PR body references their issue number). -- **Needs your review** — PRs from the review-requested query (only when subject is `@me`). - -An open issue that IS linked from an open PR body appears under "In progress" alongside that PR, not under "Up next". +## Step 3 — Classify and report (personal mode) -### 4a — Derive health badges +Sort items into these buckets: -For each open PR, derive the following badges: +- **In progress** — open PRs. Identify a linked issue from the PR body (`#N`, `closes #N`, `fixes #N`, `issue #N`) and cross-reference open issues. +- **Done** — PRs merged within the lookback window. +- **Up next** — open issues whose number is **not** referenced by any open PR body. (An issue linked from an open PR belongs under *In progress* with that PR, not here.) +- **Needs your review** — the review-requested query (only when subject is `@me`). -**Draft status:** -- If `isDraft` is `true` → `[draft]` +For each open PR, derive health from the JSON — draft state, CI status from `statusCheckRollup`, review state from `reviewDecision`, merge conflict from `mergeable`. Present each as a compact badge; omit a badge when it does not apply or is not configured. -**CI check status** (from `statusCheckRollup`): -- All checks have `status: COMPLETED` and `conclusion: SUCCESS` → `✅ checks passing` -- Any check has `conclusion: FAILURE` → `❌ checks failing` -- Checks are still running or have other states → `⏳ checks pending` -- No checks configured → omit badge +**Staleness:** flag any open PR or issue not updated within `14` days (a threshold of `0` disables staleness entirely). Note the last-updated date and age. This is a real config-driven rule — honor the threshold exactly. -**Review decision** (from `reviewDecision`): -- `APPROVED` → `✅ approved` -- `CHANGES_REQUESTED` → `🔄 changes requested` -- `REVIEW_REQUIRED` or empty → `⏳ awaiting review` +Report the buckets as a scannable summary: a heading naming the subject and lookback, a one-line count roll-up, then a section per non-empty bucket, then the local commits (or a note that git was skipped). Show labels/milestone only when present; dates as `YYYY-MM-DD`. If every GitHub bucket is empty, say so plainly for the subject and window. -**Merge conflict** (from `mergeable`): -- `CONFLICTING` → `⚠️ conflicts` -- `MERGEABLE` or `UNKNOWN` → omit badge - -### 4b — Flag stale items - -For each open PR and open issue, check `updatedAt`. If the item has not been updated within `14` days (default: 14; disabled when set to 0), flag it as stale. Record the last-updated date and the number of days since the last update. - -A stale item gets an inline `⚠️ stale (<N>d)` badge appended after any other badges. +**Do not post, comment, write files, or take any action. Output only.** --- -## Step 5 — Output the summary - -Print a formatted summary. Use this structure: - -``` -## Status for <subject> — last <lookback> days - -<N> in progress · <N> done · <N> up next[ · <N> need your review] - -### In progress -- #<number> <title> [<labels>] [<milestone>] [draft] - PR: <url> · <check badge> · <review badge>[ · <conflict badge>][ · <stale badge>] - Linked issue: #<number> (if found) +## Step 4 — What's next (personal mode) -### Done -- #<number> <title> [<labels>] — merged <date> - PR: <url> +After the summary, append **one** actionable suggestion. Gather the extra local state you need (current branch, `git status --porcelain`, latest tag via `git describe --tags --abbrev=0`, unreleased commit count via `git rev-list <tag>..HEAD --count`, and the current branch's PR review/check state via `gh pr view` — treat a non-zero exit as "no PR for this branch"). Skip git lookups when not in a repo. -### Needs your review -- #<number> <title> (by @<author>) - PR: <url> +Evaluate these conditions **in order** and use the **first** match. This ordering is the workflow's opinion about what the operator should do next — it is load-bearing, not formatting: -### Up next -- #<number> <title> [<labels>] [<milestone>][ · <stale badge>] - Issue: <url> +| Priority | Condition | Suggestion | +|----------|-----------|------------| +| 1 | On a `feature/*` branch with uncommitted changes | You have uncommitted changes on `<branch>`. When ready, run `/submit-for-review`. | +| 2 | On a `feature/*` branch, open PR is `APPROVED` and all checks `COMPLETED` | PR #<number> is approved and checks pass. Consider running `/deploy`. | +| 3 | On a `feature/*` branch with an open PR in any other state | PR #<number> (<title>) is open and awaiting review. | +| 3.5 | Subject is `@me` and PRs request your review | Append to the current suggestion (or stand alone if nothing higher matched): You also have <N> PR(s) awaiting your review. | +| 4 | On a `feature/*` branch, no open PR, clean tree | No open PR for `<branch>`. Run `/submit-for-review` to open one. | +| 5 | On the integration branch with unreleased commits since the last tag | <N> commit(s) on `<branch>` since `<tag>`. Run `/deploy` when ready to release. | +| 6 | No open PRs and no open issues assigned to subject | Nothing in progress. Run `/start` to begin new work. | +| 7 | Open issues exist in "Up next" | Next up is #<number> (<title>). Run `/start <number>` to pick it up. | -### ⚠️ Stale -- #<number> <title> — last updated <date> (<N> days ago) +If none match, omit the "what's next" section. Omit it entirely in milestone mode. --- -Local commits (current branch): -<git log output, or "skipped — not in a git repo"> -``` -Rules: -- **Summary counts line**: show immediately after the heading. Omit zero-count segments (e.g., if nothing is done, skip that segment). "need your review" only appears when subject is `@me` and the count is > 0. -- **Health badges**: show on the second line of each "In progress" item, after the PR URL, separated by ` · `. Omit individual badges that don't apply (e.g., no conflict badge if mergeable). -- **Draft badge**: show `[draft]` inline in the first line of draft PRs, before any other badges. -- **Stale section**: a dedicated section at the bottom (before "Local commits") listing all stale items from any section, with their last-updated date and age. This gives a consolidated view. Individual items also get the inline `⚠️ stale (<N>d)` badge in their own sections. -- **"Needs your review" section**: only shown when subject is `@me` and there are PRs requesting review. Placed between "Done" and "Up next". -- Omit any section that has no items — do not show an empty heading. -- Show labels only if present; show milestone only if present. -- Dates use `YYYY-MM-DD` format. -- If all GitHub sections are empty, print: `Nothing found for <subject> in the last <lookback> days.` - -Do not post, comment, write files, or take any action. Output only. - ---- - -## Step 6 — What's next - -After the status summary, append a single actionable suggestion based on local git state and the GitHub data already fetched. - -### 6a — Gather additional local state - -Run these commands (skip if not in a git repo): - -```bash -git branch --show-current -``` - -```bash -git status --porcelain -``` - -```bash -git describe --tags --abbrev=0 -``` - -```bash -git rev-list <latest-tag>..HEAD --count -``` - -From the GitHub data fetched in Step 2, also check for the current branch's PR approval status: - -```bash -gh pr view --json number,title,url,reviewDecision,statusCheckRollup \ - --jq '{number,title,url,reviewDecision,checks: [.statusCheckRollup[]? | .status]}' -``` - -If `gh pr view` exits non-zero (no PR for current branch), note that there is no open PR. - -### 6b — Determine suggestion - -Evaluate the following conditions **in order**. Use the **first** match: - -| Priority | Condition | Output | -|----------|-----------|--------| -| 1 | On a `feature/*` branch with uncommitted changes (`git status --porcelain` is non-empty) | `What's next: You have uncommitted changes on \`<branch>\`. When ready, run \`/submit-for-review\`.` | -| 2 | On a `feature/*` branch with an open PR that has `reviewDecision: APPROVED` and all status checks are `COMPLETED` | `What's next: PR #<number> is approved and checks pass. Consider running \`/deploy\`.` | -| 3 | On a `feature/*` branch with an open PR (any other review/check state) | `What's next: PR #<number> (<title>) is open and awaiting review.` | -| 3.5 | Subject is `@me` and there are PRs requesting your review (from Step 2 query) | Append to the current suggestion (or show standalone if no higher priority matched): `You also have <N> PR(s) awaiting your review.` | -| 4 | On a `feature/*` branch with no open PR and clean working tree | `What's next: No open PR for \`<branch>\`. Run \`/submit-for-review\` to open one.` | -| 5 | On the integration branch (`dev`, `develop`, or `main` when no integration branch exists) with unreleased commits (rev-list count > 0 since last tag) | `What's next: <N> commit(s) on \`<branch>\` since \`<tag>\`. Run \`/deploy\` when ready to release.` | -| 6 | No open PRs, no open issues assigned to subject | `What's next: Nothing in progress. Run \`/start\` to begin new work.` | -| 7 | Open issues exist in "Up next" | `What's next: Next up is #<number> (<title>). Run \`/start <number>\` to pick it up.` | - -If none of the above match, omit the "What's next" section entirely. - -### 6c — Format - -Print the suggestion after a horizontal rule, below the local commits section: - -``` ---- -🧭 <suggestion text> -``` - -This section is omitted in milestone mode. - ---- - -## Milestone mode (Steps M1–M3) - -Only entered when `--milestone` or `--sprint` is detected in Step 1. - -### Step M1 — Fetch milestone issues +## Milestone mode (Steps M1–M2) +### M1 — Fetch ```bash gh issue list --milestone "<name>" --state all --limit 200 \ --json number,title,state,labels,assignees,url -``` - -If this command fails for any reason (milestone not found, auth error, etc.), report the error and stop. - -### Step M2 — Classify issues - -Fetch all open PRs to detect which issues are in progress (with health fields): - -```bash gh pr list --state open \ --json number,title,body,baseRefName,reviewDecision,statusCheckRollup,mergeable,isDraft ``` +If the issue query fails (milestone not found, auth error), report and stop. -Group issues into three buckets: - -- **Done** — `state: closed` -- **In progress** — `state: open` AND the issue number appears in any open PR body (look for `#<number>`, `closes #<number>`, `fixes #<number>`, `related to #<number>`, `issue #<number>`) -- **Not started** — `state: open` AND no open PR body references the issue number - -For in-progress issues, derive health badges from the linked PR using the same rules as Step 4a (check status, review decision, draft, conflict). - -### Step M3 — Output the summary - -``` -## Sprint: <name> - -<Y> of <total> issues closed · <Z> in progress · <W> not started - -### In progress (<Z>) -- #<number> <title> [@<assignee>] [<milestone>][ [draft]] - <url> · <check badge> · <review badge>[ · <conflict badge>] - -### Not started (<W>) -- #<number> <title> [@<assignee>] +### M2 — Classify and report -### Done (<Y>) -- #<number> <title> -``` - -Rules: -- Show "In progress" first, then "Not started", then "Done" -- Show assignee only if present; omit if unassigned -- Show URLs only for in-progress items; omit URLs for closed issues -- Show health badges on in-progress items (same derivation as Step 4a) -- If a section has no items, omit it entirely +Group the milestone's issues into three buckets: +- **Done** — `state: closed`. +- **In progress** — open, and the issue number is referenced by some open PR body (`#N`, `closes #N`, `fixes #N`, `related to #N`, `issue #N`). This "referenced by an open PR" definition is the real rule — apply it exactly. +- **Not started** — open, and no open PR references it. -Do not post, comment, write files, or take any action. Output only. +Derive health badges for in-progress issues from their linked PR (same fields as personal mode). Report as a scannable summary titled with the milestone name and a closed/in-progress/not-started roll-up. Show assignees and URLs where they add value; omit empty buckets. **Do not post, comment, write files, or take any action. Output only.** --- -## Team mode (Steps T1–T3) - -Only entered when `--team` is detected in Step 1. - -### Step T1 — Fetch all open work (run both in parallel) +## Team mode (Steps T1–T2) +### T1 — Fetch (run both concurrently) ```bash gh pr list --state open --limit 100 \ --json number,title,url,author,labels,milestone,baseRefName,body,reviewDecision,statusCheckRollup,updatedAt,mergeable,isDraft -``` - -```bash gh issue list --state open --limit 200 \ --json number,title,url,assignees,labels,milestone,updatedAt ``` +If either fails, report and stop. -If either command fails, report the error and stop. - -### Step T2 — Group and classify - -Group items by person: -- PRs are grouped by `author.login` -- Issues are grouped by assignee (first assignee if multiple). Issues with no assignee go into an "Unassigned" group. - -Within each person's group, classify items the same way as personal mode (Step 4): -- **In progress** — open PRs (and linked issues) -- **Up next** — open issues not linked from any open PR - -Derive health badges (Step 4a) and flag stale items (Step 4b) for all items. - -### Step T3 — Output the team summary - -``` -## Team status - -<N> open PRs · <N> open issues · <N> people - -### @<person> (<N> in progress, <N> up next) -- #<number> <title> — PR <check badge> · <review badge>[ · <conflict badge>][ · <stale badge>][ [draft]] - <url> -- #<number> <title> [up next][ · <stale badge>] - -### @<person> (<N> in progress, <N> up next) -... - -### Unassigned (<N>) -- #<number> <title> - <url> - -### ⚠️ Stale -- #<number> <title> (@<person>) — last updated <date> (<N> days ago) -``` - -Rules: -- Sort people alphabetically by username -- Within each person, show in-progress items first, then up-next items -- Show health badges on PR items (same format as personal mode) -- Show `[draft]` on draft PRs -- Tag up-next items with `[up next]` for visual distinction -- "Unassigned" section appears at the bottom, only if there are unassigned issues -- "Stale" section consolidates all stale items across all people -- Omit any section or group with no items -- No "What's next" section in team mode +### T2 — Group and report -Do not post, comment, write files, or take any action. Output only. +Group by person: PRs by `author.login`; issues by first assignee (unassigned issues into an "Unassigned" group). Within each person, classify as personal mode does — **in progress** (open PRs and their linked issues) and **up next** (open issues not referenced by any open PR) — and derive health badges plus staleness. Report per-person sections with in-progress items first, an "Unassigned" section if any, and a consolidated stale section. **Do not post, comment, write files, or take any action. Output only.** --- ## Hard rules -- Never write to GitHub (no comments, labels, issue updates, or PR changes). -- 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. -<!-- generated by CodeCannon/sync.py | skill: status | adapter: codex | hash: 5832e667 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +- Never write to GitHub (no comments, labels, issue updates, or PR changes) and never touch the working tree. Output only. +- If `gh` is unauthenticated or any fetch fails, report the error and stop immediately. Do not retry. +- Strip the leading `@` from the subject when passing to `gh` flags that reject it. +- `--team` is mutually exclusive with `--milestone`/`--sprint` and with a username. +- The `14` threshold is config-driven; `0` disables staleness. The "what's next" priority ordering is fixed — evaluate top to bottom, first match wins. +<!-- generated by CodeCannon/sync.py | skill: status | adapter: codex | hash: beebb032 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.claude/commands/deploy.md b/.claude/commands/deploy.md index d9d6ceb..dcc543c 100644 --- a/.claude/commands/deploy.md +++ b/.claude/commands/deploy.md @@ -4,25 +4,24 @@ Code Cannon: Bump the project version, create a GitHub Release, and promote to p ## What `/deploy` does -`/deploy` is the final step in the workflow. It combines version bumping and release creation into a single command: check state, optionally bump the version, then create a GitHub Release (and in multi-branch mode, promote to production). +`/deploy` is the final step in the workflow. It combines version bumping and release creation into a single command: check state, optionally bump the version, then create a GitHub Release (and in multi-branch mode, promote the deploy branch to production first). + +The branching mode changes the shape of the release: in **trunk mode** (`BRANCH_PROD` only) `/deploy` tags and releases the current branch directly; in **multi-branch mode** (`BRANCH_DEV` set, optionally with `BRANCH_TEST`) it first opens and merges a release PR from the deploy branch into production, and that merge is what closes the linked issues. --- -## Step 1 — Verify branch +## Step 1 — Verify branch and sync -Run: -```bash -git branch --show-current -``` +Run `git branch --show-current`. The **deploy branch** for this project is: -Required branch: `dev` (two-branch mode). +`dev` (two-branch mode). -If not on the required branch, abort and say: "Switch to `<required-branch>` before running `/deploy`." +If not on the deploy branch, abort: "Switch to `<deploy-branch>` before running `/deploy`." -Sync to the remote before proceeding. The script below guards against uncommitted local changes, then runs `git checkout`, `git fetch`, and `git reset --hard origin/<base>` as one atomic operation. The deploy branch is never edited locally under the CodeCannon workflow (only fast-forwarded from CodeCannon's own merges), so the hard reset is the correct sync; the dirty-tree guard catches accidental local edits before they get silently discarded. +Then sync it to the remote. The script guards against uncommitted local changes, then runs `git checkout`, `git fetch`, and `git reset --hard origin/<deploy-branch>` as one atomic operation. The deploy branch is never edited locally under the CodeCannon workflow (only fast-forwarded from merges), so the hard reset is the correct sync; the dirty-tree guard catches accidental local edits before they are silently discarded. ```bash -python3 CodeCannon/skills/github-agile/scripts/sync-base-branch.py dev +python3 CodeCannon/skills/github-agile/scripts/sync-base-branch.py <deploy-branch> ``` If the script exits non-zero, stop and resolve the issue it reports before continuing. @@ -31,87 +30,35 @@ If the script exits non-zero, stop and resolve the issue it reports before conti ## Step 2 — Check current state -### Find the latest version tag - -```bash -git describe --tags --abbrev=0 -``` +Find the latest version tag (`git describe --tags --abbrev=0`; if none, note this is the first release) and read the current version with `cat VERSION`. -If no tag exists, note this is the first release. - -### Read current version +Show the merge commits (and their PRs) since the last tag. The range depends on the mode: ```bash -cat VERSION +git log main..<deploy-branch> --merges --pretty=format:"%s" ``` -### Show commits since last tag +Merge-commit subjects have the form `Merge pull request #N from branch/name` — parse the PR numbers, then retrieve each body with `gh pr view <N> --json number,title,body`. -If a previous tag exists, show what's on the branch since that tag: +From those PR bodies, compile the release's issue links: -```bash -git log main..dev --merges --pretty=format:"%s" -``` - -Parse PR numbers from merge commit subjects (format: `Merge pull request #N from branch/name`). +Keep closing keywords and context references **separate — do not merge them into one set**: -For each PR number found, retrieve the PR body: -```bash -gh pr view <N> --json number,title,body -``` +- **Close set** — the union of every `Closes #N` line across all constituent PR bodies. These auto-close when the release PR merges into `main`. Record, per constituent PR, the exact `Closes #N` lines so they can be reproduced verbatim in the release PR body. +- **Reference set** — issues mentioned only via `Related to #N` or the legacy `Issue #N` form. These are context links and will **not** close. Legacy `Issue #N` carries no recoverable close-intent, so it stays in the reference set rather than being guessed into the close set; the human gate surfaces it so you can manually close any straggler that should have closed. +- **PRs included** (number + title). -Extract closing keywords **separately** from context references — do **not** merge them into a single set: +Also check for open unmerged PRs (`gh pr list --state open --json number,title,headRefName`). -- **Close set** — the union of every `Closes #N` line across all constituent PR bodies. These issues will auto-close when the release PR merges into `main`. Record, per constituent PR, the exact `Closes #N` lines it contained so they can be reproduced verbatim in the release PR body. -- **Reference set** — issues mentioned only via `Related to #N`, or via the legacy `Issue #N` form. These are context links and will **not** close. Legacy `Issue #N` carries no recoverable close-intent, so it stays in the reference set rather than being guessed into the close set; the HUMAN GATE surfaces it so you can manually close any straggler that should have been a `Closes`. - -Compile: -- List of PRs included (number + title) -- Close set and reference set, kept distinct - -### Check for open unmerged PRs - -```bash -gh pr list --state open --json number,title,headRefName -``` - -### Present the summary - -Tell the user: - -``` -Current version: X.Y.Z -Latest tag: vX.Y.Z - -Commits/PRs since last tag: - #17 — Add /docs directory - #18 — Fix checkout runtime error - -Open PRs not yet merged: - #19 — Add dark mode (feature/dark-mode) - -Would you like to bump the version before deploying? - - **patch** → X.Y.C - - **minor** → X.B.0 - - **major** → A.0.0 - - **specific** → enter a version number - - **skip** → proceed to release with the latest existing tag -``` - -Wait for their response. +Present a summary — current version, latest tag, the PRs/issues since that tag, any open PRs — and ask whether to bump the version before deploying (patch → X.Y.C, minor → X.B.0, major → A.0.0, a specific version, or skip to release the latest existing tag). Wait for their response. --- ## Step 3 — Version bump (if requested) -If the user chose to skip, find the latest version tag in the branch history: -```bash -git describe --tags --abbrev=0 -``` - -If no tag is found at all (first release), warn: "No version tag found. You must bump the version before deploying." Return to the version bump prompt. Otherwise, use the tag found as the release version. +If the user chose **skip**, use the latest existing tag (`git describe --tags --abbrev=0`) as the release version. If none exists (first release), warn "No version tag found. You must bump the version before deploying." and return to the bump prompt. -If the user chose a bump level, map their response to a bump command and run `bump-and-tag.py`, which performs the bump, verifies the resulting tag (creating an annotated fallback if `tag.forceSignAnnotated` silently rejected a lightweight tag), and pushes both the commit and the tag. The resolved version is printed on stdout — capture it for the release step. +If the user chose a bump level, map it to a bump command and run `bump-and-tag.py`, which performs the bump, verifies the resulting tag (creating an annotated fallback if `tag.forceSignAnnotated` silently rejected a lightweight tag), and pushes both the commit and the tag. The resolved version is printed on stdout — capture it as `<new-version>`. | User says | `--bump-cmd` | |---|---| @@ -126,59 +73,37 @@ python3 CodeCannon/skills/github-agile/scripts/bump-and-tag.py \ --version-read-cmd "cat VERSION" ``` -If the script exits non-zero, stop and resolve the issue it reports before continuing. On success, the version printed on stdout is the new version — use it as `<new-version>` in subsequent steps. +If the script exits non-zero, stop and resolve the issue it reports before continuing. --- ## Step 4 — Compute release contents -Determine the version tag (either from the bump just performed, or from the existing HEAD tag if the user skipped bumping). +Determine the release version tag (from the bump just performed, or the existing HEAD tag if the user skipped). Find the previous tag for the changelog range: `git describe --abbrev=0 <version-tag>^`. -Find the previous tag to determine the range: -```bash -git describe --abbrev=0 <version-tag>^ -``` - -Use the PR list, close set, and reference set already computed in Step 2. If the version bump added new commits, re-fetch if needed. +Reuse the PR list, close set, and reference set already computed in Step 2. If the version bump added new commits, re-fetch as needed. --- ## Step 5 — HUMAN GATE -Show the user the release summary. Example format: - -``` -Ready to release vX.Y.Z to production. +Show the release summary — the target version, the PRs included, and the issue links: -PRs included: - #17 — Add /docs directory - #18 — Fix checkout runtime error +- Issues that will **close** on merge (the close set, reproduced verbatim from constituent PRs). +- Issues **referenced but not closing** (the reference set — confirm none of these should actually close). -Issues that will close on merge (Closes #N, reproduced verbatim from constituent PRs): - #14 — Add /docs directory - #15 — Fix checkout runtime error - -Issues referenced but NOT closing (Related to #N / legacy Issue #N — confirm none of these should actually close): - #20 — Tighten error copy on the upload form - -Have you tested all of the above on preview? Type 'release' to confirm. -``` +Confirm the deploy branch has been tested: +"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. +Wait for "release" or an explicit confirmation. Any other response → stop and ask what they'd like to change. --- -## Step 6 — Create PR: `dev` → `main` +## Step 6 — Promote: `<deploy-branch>` → `main` -First, create a temp directory for this invocation: +Create a temp directory for this invocation (`python3 CodeCannon/skills/github-agile/scripts/make-workdir.py`) and note the returned path — use it for all temp files here. -```bash -python3 CodeCannon/skills/github-agile/scripts/make-workdir.py -``` - -Note the returned path (e.g. `/tmp/CodeCannon/a8f3b2`). Use this path for all temp files in this invocation. - -Then use your file-writing tool (not Bash) to create `<tmpdir>/release_pr_body.md`: +Use your file-writing tool (not Bash) to create `<tmpdir>/release_pr_body.md`: ```markdown Release vX.Y.Z @@ -193,53 +118,31 @@ Closes #15 Related to #20 ``` -Reproduce **every** `Closes #N` line from the close set computed in Step 2 — verbatim, one per line, omitting none. Add a `Related to #N` line for each issue in the reference set so the links appear on the release PR without triggering an auto-close. If the reference set is empty, omit the `Related to` lines entirely. +Reproduce **every** `Closes #N` line from the close set — verbatim, one per line, omitting none. Add a `Related to #N` line for each issue in the reference set so the links appear without triggering an auto-close; if the reference set is empty, omit the `Related to` lines entirely. -Then create the PR (do NOT use `--body`, `--body-file -`, or heredocs): +Create the PR (do NOT use `--body`, `--body-file -`, or heredocs), with `--head` set to the deploy branch: ```bash -gh pr create --base main --head dev \ +gh pr create --base main --head <deploy-branch> \ --title "Release vX.Y.Z" \ --body-file <tmpdir>/release_pr_body.md ``` -Note the PR number from the output. - -The `Closes #N` lines will auto-close the linked issues because this PR merges into `main` (the default branch). - -> **Critical:** Use the unqualified `#N` form only. Never write `Closes owner/repo#N`, even for same-repo refs — GitHub's closing-keyword parser only populates `closingIssuesReferences` for the unqualified form, and the qualified form silently breaks auto-close. - ---- - -## Step 7 — Merge +> **Critical:** Use the unqualified `#N` form only. Never write `Closes owner/repo#N`, even for same-repo refs — GitHub's closing-keyword parser only populates `closingIssuesReferences` for the unqualified form, and the qualified form silently breaks auto-close. The `Closes #N` lines auto-close the linked issues because this PR merges into `main` (the default branch). -Do NOT use `make merge` — it refuses PRs targeting `main`. Use `gh pr merge` directly: - -```bash -gh pr merge <pr-number> --merge -``` +Then merge. Do NOT use `make merge` — it refuses PRs targeting `main`. Use `gh pr merge <pr-number> --merge` directly. --- -## Step 8 — Create GitHub Release +## Step 7 — Create the GitHub Release -**Publish confirmation — required before writing the release notes or creating the Release.** The GitHub Release is a public-surface action. The single word from Step 5 authorized the promotion/merge (already done); the public publish gets its own explicit confirmation. Tell the user (substitute the actual tag, e.g. `v0.13.0`): +**Publish confirmation — required before writing the release notes or creating the Release.** The GitHub Release is a public-surface action; the confirmation from Step 5 authorized the promotion, but the public publish gets its own explicit confirmation. Tell the user (substitute the actual tag, e.g. `v0.13.0`): > Publishing GitHub Release `<version-tag>` — the final public step. Confirm by pasting: `publish <version-tag>` Wait for the user to paste `publish <version-tag>` (or an explicit version-named variant such as `ship <version-tag>`). Any other response → stop and ask what they'd like to change. The version-named phrase is deliberate: Claude Code's auto-mode safety classifier requires authorization that names the release before `gh release create` runs, so the generic Step 5 confirmation is not relied on for the public publish. If a harness still blocks the call after this confirmation (e.g. an older client), the user can re-confirm with `publish <version-tag> release` to unblock. ---- - -The version tag (from Step 3) and the PR/issue list (from Step 4) are already known. Find the previous tag to build the changelog link: - -```bash -git describe --abbrev=0 <version-tag>^ -``` - -If no previous tag exists, omit the "Full changelog" line. - -Use your file-writing tool (not Bash) to create `<tmpdir>/release_notes.md` (same temp directory from Step 6): +The version tag and PR/issue list are already known; the previous tag comes from Step 4 (if there is no previous tag, omit the "Full changelog" line). Create a temp directory if you haven't already (`python3 CodeCannon/skills/github-agile/scripts/make-workdir.py`), then use your file-writing tool (not Bash) to create `<tmpdir>/release_notes.md`: ```markdown ## Changes @@ -250,7 +153,7 @@ Use your file-writing tool (not Bash) to create `<tmpdir>/release_notes.md` (sam **Full changelog:** https://github.com/<owner>/<repo>/compare/<previous-tag>...<version-tag> ``` -Then create the release (do NOT use `--notes`, `--notes-file -`, or heredocs): +Format each PR line as `- #<linked-issue> — <PR title> (PR #<N>)`; if a PR had no linked issue, use just the PR title. Then create the release (do NOT use `--notes`, `--notes-file -`, or heredocs): ```bash gh release create <version-tag> \ @@ -258,15 +161,11 @@ gh release create <version-tag> \ --notes-file <tmpdir>/release_notes.md ``` -Format each PR line as `- #<linked-issue> — <PR title> (PR #<N>)`. If a PR had no linked issue, omit the `#<issue>` prefix and use just the PR title. - -After the command runs, note the release URL from the output. +Note the release URL from the output. --- -## Step 9 — Report - -Tell the user: +## Step 8 — Report -> "Released vX.Y.Z. Issues #N, #M closed automatically. GitHub Release vX.Y.Z created at `<url>`. Run `make deploy-prod` to ship to production." -<!-- generated by CodeCannon/sync.py | skill: deploy | adapter: claude | hash: 790acdbf | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +Tell the user: "Released vX.Y.Z. Linked issues are closed. GitHub Release vX.Y.Z created at `<url>`. Run `make deploy-prod` to ship to production." +<!-- generated by CodeCannon/sync.py | skill: deploy | adapter: claude | hash: 8f580805 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.claude/commands/start.md b/.claude/commands/start.md index ac17937..bc69cc3 100644 --- a/.claude/commands/start.md +++ b/.claude/commands/start.md @@ -22,12 +22,10 @@ Otherwise → go to **Case A: New work**. > **Execution order:** Resolve labels and milestones **now**, before entering Case A Step 1. If milestone auto-detection requires a user prompt (2+ open milestones), that prompt happens here — not later during issue creation. By the time you reach Step 2's human gate, all metadata must already be resolved so that Step 3 can proceed without re-prompting. -The argument string may contain optional inline flags after the description. Parse as follows: +The description may be followed by optional flags — `--label`/`-l` and `--milestone`/`-m`, in any order. Separate the description from the flags yourself; the flags carry these meanings: -1. **Identify flags** — scan for the first token that starts with `--label`, `-l`, `--milestone`, or `-m`. Everything before it is the **description**. Everything from the first flag onward is **flags**. -2. **`--label <value>` / `-l <value>`** — comma-separated label string (e.g. `bug` or `enhancement,ux`). If provided, it **bypasses label auto-selection entirely** for this invocation — use the value verbatim. Labels containing spaces must be quoted (e.g. `--label "good first issue"`). -3. **`--milestone <value>` / `-m <value>`** — milestone name or number (e.g. `Sprint 4` or `12`). Pass the value as-is; GitHub accepts both names and numbers. -4. **Flags may appear in any order** after the description. +- **`--label <value>` / `-l <value>`** — a comma-separated label string used **verbatim**, bypassing label auto-selection entirely for this invocation. Quote values containing spaces (e.g. `--label "good first issue"`). +- **`--milestone <value>` / `-m <value>`** — a milestone name or number (GitHub accepts both names and numbers). **Label resolution (three-tier, Case A only):** @@ -52,14 +50,6 @@ After parsing flags, determine the active milestone in this order: - **1 result** → use its title silently. Inform the user inline: `(milestone: <title>)`. - **2+ results** → show the numbered list, ask once: **"Multiple open milestones — which should this issue go under? (enter a number or title, or 'none')"**. Accept milestone number, title, or "none"/"skip". Wait for response before continuing. -**Examples:** - -| `$ARGUMENTS` | Description | Labels | Milestone | -|---|---|---|---| -| `Add dark mode toggle to settings page` | `Add dark mode toggle to settings page` | auto-selected from pool | auto-detected | -| `Add dark mode --label enhancement` | `Add dark mode` | `enhancement` (verbatim) | auto-detected | -| `Add dark mode --label enhancement,ux --milestone "Sprint 4"` | `Add dark mode` | `enhancement,ux` (verbatim) | `Sprint 4` | - > Replace vs append: flags **replace** auto-selection entirely, they do not append. This avoids silent label duplication and milestone conflicts. --- @@ -97,7 +87,7 @@ If on any other branch → proceed to Case A or Case B as determined by the `$AR ### Step 1 — Investigate -Read the relevant code. Propose a concrete implementation approach. Be specific about which files change and how. +Read the relevant code using your harness's native file-reading and search tools (read, grep/glob, and the like) rather than shell pipelines — hand-rolled `find … | xargs`, `grep ; awk`, or redirection shapes trigger permission prompts that cannot be permanently allowed. Then propose a concrete implementation approach, specific about which files change and how. ### Step 2 — HUMAN GATE @@ -217,7 +207,14 @@ Show the user: `On branch feature/<name>` ### Step 5 — Write the code -Now write the code. Do NOT commit anything. +Write the code using your harness's native editing tools. Do NOT commit anything. +To exercise your work as you go, run the project's configured test command rather than hand-rolling a test invocation — a hand-built `python3 -m unittest … > /tmp/…` or similar redirection shape triggers permission prompts that a configured command avoids: + +```bash +make test +``` + +If it fails because the target does not exist, tell the user rather than improvising a replacement. When done, say: **"When you've verified locally, reply `yes` to submit, or say what to change."** @@ -312,7 +309,14 @@ git branch --show-current ### Step 5 — Write the code -Continue from where work left off. Do NOT commit. +Continue from where work left off, using your harness's native editing tools. Do NOT commit. +To exercise your work as you go, run the project's configured test command rather than hand-rolling a test invocation — a hand-built `python3 -m unittest … > /tmp/…` or similar redirection shape triggers permission prompts that a configured command avoids: + +```bash +make test +``` + +If it fails because the target does not exist, tell the user rather than improvising a replacement. When done, say: **"When you've verified locally, reply `yes` to submit, or say what to change."** @@ -331,4 +335,4 @@ When done, say: **"When you've verified locally, reply `yes` to submit, or say w - 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 <number> --remove-assignee @me`. - Apply resolved labels and milestone to every new issue. Label resolution order: per-invocation flag → pool selection from `bug, documentation, enhancement, chore` → omit `--label` entirely. Never apply a label outside `bug, documentation, enhancement, chore`. - Milestone resolution order: per-invocation flag → auto-detected from GitHub open milestones. Never prompt for a milestone more than once per invocation. -<!-- generated by CodeCannon/sync.py | skill: start | adapter: claude | hash: 2c9789e5 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: start | adapter: claude | hash: 357ff5d9 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.claude/commands/status.md b/.claude/commands/status.md index 0f085ea..88d4203 100644 --- a/.claude/commands/status.md +++ b/.claude/commands/status.md @@ -2,45 +2,39 @@ Code Cannon: Summarize in-progress and recently completed work from GitHub and g --- -## Step 1 — Parse arguments +## What `/status` does -First, check whether `$ARGUMENTS` contains `--milestone`, `--sprint`, or `--team`. +`/status` prints a read-only, standup-ready snapshot of in-progress and recently completed work, then a single "what's next" suggestion. It never writes to GitHub or the working tree — it only reads and reports. -**Milestone mode:** If `--milestone` or `--sprint` is present, extract everything after the flag as the milestone name (trim leading/trailing whitespace; preserve internal spaces). Ignore any other arguments. Enter milestone mode (Steps M1–M3 below) and skip Steps 2–6. +Because it is read-only, the *shape* of its output does not matter: a differently-formatted-but-accurate summary is a fine result. Derive a clear, scannable layout yourself. What this skill pins down is the data to fetch, how to classify it, and the one piece of real opinion — the "what's next" ordering. -Examples: -- `--milestone Sprint 4` → milestone name = `Sprint 4` -- `--sprint Sprint 4` → milestone name = `Sprint 4` -- `--milestone Q2 Release` → milestone name = `Q2 Release` -- `--milestone 12` → milestone name = `12` - -**Team mode:** If `--team` is present, enter team mode (Steps T1–T3 below) and skip Steps 2–6. `--team` is mutually exclusive with `--milestone`/`--sprint` and username arguments. If both are present, report the conflict and stop. +--- -**Personal mode** (no `--milestone` / `--sprint` / `--team` flag): determine: +## Step 1 — Determine mode -- **subject**: default `@me`. If the argument starts with `@` or is a plain word that is not a number, treat it as a GitHub username. Strip the leading `@` for `gh` commands that do not accept it (e.g. `gh pr list --author alice`); keep it for display. -- **lookback**: default `7`. If the argument is a number (digits only), use it as the lookback window in days. +Three mutually exclusive modes, selected from `$ARGUMENTS`: -No argument → subject = `@me`, lookback = `7`. +- **Milestone mode** — `--milestone` or `--sprint` is present. Everything after the flag is the milestone name (a name or a number; trim outer whitespace, preserve internal spaces). Ignore other arguments. Run Steps M1–M2. +- **Team mode** — `--team` is present. Run Steps T1–T2. `--team` is mutually exclusive with `--milestone`/`--sprint` and with a username; if combined, report the conflict and stop. +- **Personal mode** — no mode flag. **Subject** defaults to `@me`; a `@name` or a non-numeric word is a username (strip the leading `@` for `gh` flags that reject it, keep it for display). **Lookback** defaults to `7`; a bare number is the lookback in days. --- -## Step 2 — Fetch GitHub data (run all in parallel) +## Step 2 — Fetch GitHub data (personal mode) -Run these commands concurrently: +Run these concurrently. If any `gh` command exits non-zero (including auth errors), report the message and stop — do not retry. -**Open PRs authored by subject:** +**Open PRs authored by subject** — request enough fields to derive health (draft, CI, review decision, merge conflict) and staleness: ```bash gh pr list --author <subject> --state open \ --json number,title,url,labels,milestone,baseRefName,body,reviewDecision,statusCheckRollup,updatedAt,mergeable,isDraft ``` -**Recently merged PRs (last `<lookback>` days):** +**Recently merged PRs**, filtered to those merged within `<lookback>` days: ```bash gh pr list --author <subject> --state merged --limit 20 \ --json number,title,url,mergedAt,labels,baseRefName ``` -Filter the results to keep only entries where `mergedAt` is within the last `<lookback>` days. **Open issues assigned to subject:** ```bash @@ -48,317 +42,103 @@ gh issue list --assignee <subject> --state open \ --json number,title,url,labels,milestone,updatedAt ``` -**PRs requesting your review** (only when subject is `@me`): +**PRs requesting your review** — only when subject is `@me`; skip for other users: ```bash gh pr list --search "review-requested:@me" --state open \ --json number,title,url,author,updatedAt ``` -Skip this query when viewing another user's status. -If any `gh` command exits with a non-zero status (including auth errors), report the error message and stop. Do not retry. - ---- - -## Step 3 — Fetch local git context - -Check if the current directory is inside a git repository: -```bash -git rev-parse --is-inside-work-tree -``` - -If yes, run: +Also fetch local git context (skip and note if not in a git repo — `git rev-parse --is-inside-work-tree`): ```bash git log --oneline --since="<lookback> days ago" ``` -If not inside a git repo, skip this step and note it was skipped in the output. - --- -## Step 4 — Classify items - -Using the data from Steps 2 and 3, classify each item: - -- **In progress** — open PRs. For each, attempt to identify a linked issue number from the PR body (look for `#N`, `closes #N`, `fixes #N`, `issue #N`). If found, cross-reference with open issues. -- **Done** — merged PRs within the lookback window. -- **Up next** — open issues that are NOT associated with any open PR (i.e. no open PR body references their issue number). -- **Needs your review** — PRs from the review-requested query (only when subject is `@me`). - -An open issue that IS linked from an open PR body appears under "In progress" alongside that PR, not under "Up next". +## Step 3 — Classify and report (personal mode) -### 4a — Derive health badges +Sort items into these buckets: -For each open PR, derive the following badges: +- **In progress** — open PRs. Identify a linked issue from the PR body (`#N`, `closes #N`, `fixes #N`, `issue #N`) and cross-reference open issues. +- **Done** — PRs merged within the lookback window. +- **Up next** — open issues whose number is **not** referenced by any open PR body. (An issue linked from an open PR belongs under *In progress* with that PR, not here.) +- **Needs your review** — the review-requested query (only when subject is `@me`). -**Draft status:** -- If `isDraft` is `true` → `[draft]` +For each open PR, derive health from the JSON — draft state, CI status from `statusCheckRollup`, review state from `reviewDecision`, merge conflict from `mergeable`. Present each as a compact badge; omit a badge when it does not apply or is not configured. -**CI check status** (from `statusCheckRollup`): -- All checks have `status: COMPLETED` and `conclusion: SUCCESS` → `✅ checks passing` -- Any check has `conclusion: FAILURE` → `❌ checks failing` -- Checks are still running or have other states → `⏳ checks pending` -- No checks configured → omit badge +**Staleness:** flag any open PR or issue not updated within `14` days (a threshold of `0` disables staleness entirely). Note the last-updated date and age. This is a real config-driven rule — honor the threshold exactly. -**Review decision** (from `reviewDecision`): -- `APPROVED` → `✅ approved` -- `CHANGES_REQUESTED` → `🔄 changes requested` -- `REVIEW_REQUIRED` or empty → `⏳ awaiting review` +Report the buckets as a scannable summary: a heading naming the subject and lookback, a one-line count roll-up, then a section per non-empty bucket, then the local commits (or a note that git was skipped). Show labels/milestone only when present; dates as `YYYY-MM-DD`. If every GitHub bucket is empty, say so plainly for the subject and window. -**Merge conflict** (from `mergeable`): -- `CONFLICTING` → `⚠️ conflicts` -- `MERGEABLE` or `UNKNOWN` → omit badge - -### 4b — Flag stale items - -For each open PR and open issue, check `updatedAt`. If the item has not been updated within `14` days (default: 14; disabled when set to 0), flag it as stale. Record the last-updated date and the number of days since the last update. - -A stale item gets an inline `⚠️ stale (<N>d)` badge appended after any other badges. +**Do not post, comment, write files, or take any action. Output only.** --- -## Step 5 — Output the summary - -Print a formatted summary. Use this structure: - -``` -## Status for <subject> — last <lookback> days - -<N> in progress · <N> done · <N> up next[ · <N> need your review] - -### In progress -- #<number> <title> [<labels>] [<milestone>] [draft] - PR: <url> · <check badge> · <review badge>[ · <conflict badge>][ · <stale badge>] - Linked issue: #<number> (if found) +## Step 4 — What's next (personal mode) -### Done -- #<number> <title> [<labels>] — merged <date> - PR: <url> +After the summary, append **one** actionable suggestion. Gather the extra local state you need (current branch, `git status --porcelain`, latest tag via `git describe --tags --abbrev=0`, unreleased commit count via `git rev-list <tag>..HEAD --count`, and the current branch's PR review/check state via `gh pr view` — treat a non-zero exit as "no PR for this branch"). Skip git lookups when not in a repo. -### Needs your review -- #<number> <title> (by @<author>) - PR: <url> +Evaluate these conditions **in order** and use the **first** match. This ordering is the workflow's opinion about what the operator should do next — it is load-bearing, not formatting: -### Up next -- #<number> <title> [<labels>] [<milestone>][ · <stale badge>] - Issue: <url> +| Priority | Condition | Suggestion | +|----------|-----------|------------| +| 1 | On a `feature/*` branch with uncommitted changes | You have uncommitted changes on `<branch>`. When ready, run `/submit-for-review`. | +| 2 | On a `feature/*` branch, open PR is `APPROVED` and all checks `COMPLETED` | PR #<number> is approved and checks pass. Consider running `/deploy`. | +| 3 | On a `feature/*` branch with an open PR in any other state | PR #<number> (<title>) is open and awaiting review. | +| 3.5 | Subject is `@me` and PRs request your review | Append to the current suggestion (or stand alone if nothing higher matched): You also have <N> PR(s) awaiting your review. | +| 4 | On a `feature/*` branch, no open PR, clean tree | No open PR for `<branch>`. Run `/submit-for-review` to open one. | +| 5 | On the integration branch with unreleased commits since the last tag | <N> commit(s) on `<branch>` since `<tag>`. Run `/deploy` when ready to release. | +| 6 | No open PRs and no open issues assigned to subject | Nothing in progress. Run `/start` to begin new work. | +| 7 | Open issues exist in "Up next" | Next up is #<number> (<title>). Run `/start <number>` to pick it up. | -### ⚠️ Stale -- #<number> <title> — last updated <date> (<N> days ago) +If none match, omit the "what's next" section. Omit it entirely in milestone mode. --- -Local commits (current branch): -<git log output, or "skipped — not in a git repo"> -``` -Rules: -- **Summary counts line**: show immediately after the heading. Omit zero-count segments (e.g., if nothing is done, skip that segment). "need your review" only appears when subject is `@me` and the count is > 0. -- **Health badges**: show on the second line of each "In progress" item, after the PR URL, separated by ` · `. Omit individual badges that don't apply (e.g., no conflict badge if mergeable). -- **Draft badge**: show `[draft]` inline in the first line of draft PRs, before any other badges. -- **Stale section**: a dedicated section at the bottom (before "Local commits") listing all stale items from any section, with their last-updated date and age. This gives a consolidated view. Individual items also get the inline `⚠️ stale (<N>d)` badge in their own sections. -- **"Needs your review" section**: only shown when subject is `@me` and there are PRs requesting review. Placed between "Done" and "Up next". -- Omit any section that has no items — do not show an empty heading. -- Show labels only if present; show milestone only if present. -- Dates use `YYYY-MM-DD` format. -- If all GitHub sections are empty, print: `Nothing found for <subject> in the last <lookback> days.` - -Do not post, comment, write files, or take any action. Output only. - ---- - -## Step 6 — What's next - -After the status summary, append a single actionable suggestion based on local git state and the GitHub data already fetched. - -### 6a — Gather additional local state - -Run these commands (skip if not in a git repo): - -```bash -git branch --show-current -``` - -```bash -git status --porcelain -``` - -```bash -git describe --tags --abbrev=0 -``` - -```bash -git rev-list <latest-tag>..HEAD --count -``` - -From the GitHub data fetched in Step 2, also check for the current branch's PR approval status: - -```bash -gh pr view --json number,title,url,reviewDecision,statusCheckRollup \ - --jq '{number,title,url,reviewDecision,checks: [.statusCheckRollup[]? | .status]}' -``` - -If `gh pr view` exits non-zero (no PR for current branch), note that there is no open PR. - -### 6b — Determine suggestion - -Evaluate the following conditions **in order**. Use the **first** match: - -| Priority | Condition | Output | -|----------|-----------|--------| -| 1 | On a `feature/*` branch with uncommitted changes (`git status --porcelain` is non-empty) | `What's next: You have uncommitted changes on \`<branch>\`. When ready, run \`/submit-for-review\`.` | -| 2 | On a `feature/*` branch with an open PR that has `reviewDecision: APPROVED` and all status checks are `COMPLETED` | `What's next: PR #<number> is approved and checks pass. Consider running \`/deploy\`.` | -| 3 | On a `feature/*` branch with an open PR (any other review/check state) | `What's next: PR #<number> (<title>) is open and awaiting review.` | -| 3.5 | Subject is `@me` and there are PRs requesting your review (from Step 2 query) | Append to the current suggestion (or show standalone if no higher priority matched): `You also have <N> PR(s) awaiting your review.` | -| 4 | On a `feature/*` branch with no open PR and clean working tree | `What's next: No open PR for \`<branch>\`. Run \`/submit-for-review\` to open one.` | -| 5 | On the integration branch (`dev`, `develop`, or `main` when no integration branch exists) with unreleased commits (rev-list count > 0 since last tag) | `What's next: <N> commit(s) on \`<branch>\` since \`<tag>\`. Run \`/deploy\` when ready to release.` | -| 6 | No open PRs, no open issues assigned to subject | `What's next: Nothing in progress. Run \`/start\` to begin new work.` | -| 7 | Open issues exist in "Up next" | `What's next: Next up is #<number> (<title>). Run \`/start <number>\` to pick it up.` | - -If none of the above match, omit the "What's next" section entirely. - -### 6c — Format - -Print the suggestion after a horizontal rule, below the local commits section: - -``` ---- -🧭 <suggestion text> -``` - -This section is omitted in milestone mode. - ---- - -## Milestone mode (Steps M1–M3) - -Only entered when `--milestone` or `--sprint` is detected in Step 1. - -### Step M1 — Fetch milestone issues +## Milestone mode (Steps M1–M2) +### M1 — Fetch ```bash gh issue list --milestone "<name>" --state all --limit 200 \ --json number,title,state,labels,assignees,url -``` - -If this command fails for any reason (milestone not found, auth error, etc.), report the error and stop. - -### Step M2 — Classify issues - -Fetch all open PRs to detect which issues are in progress (with health fields): - -```bash gh pr list --state open \ --json number,title,body,baseRefName,reviewDecision,statusCheckRollup,mergeable,isDraft ``` +If the issue query fails (milestone not found, auth error), report and stop. -Group issues into three buckets: - -- **Done** — `state: closed` -- **In progress** — `state: open` AND the issue number appears in any open PR body (look for `#<number>`, `closes #<number>`, `fixes #<number>`, `related to #<number>`, `issue #<number>`) -- **Not started** — `state: open` AND no open PR body references the issue number - -For in-progress issues, derive health badges from the linked PR using the same rules as Step 4a (check status, review decision, draft, conflict). - -### Step M3 — Output the summary - -``` -## Sprint: <name> - -<Y> of <total> issues closed · <Z> in progress · <W> not started - -### In progress (<Z>) -- #<number> <title> [@<assignee>] [<milestone>][ [draft]] - <url> · <check badge> · <review badge>[ · <conflict badge>] - -### Not started (<W>) -- #<number> <title> [@<assignee>] +### M2 — Classify and report -### Done (<Y>) -- #<number> <title> -``` - -Rules: -- Show "In progress" first, then "Not started", then "Done" -- Show assignee only if present; omit if unassigned -- Show URLs only for in-progress items; omit URLs for closed issues -- Show health badges on in-progress items (same derivation as Step 4a) -- If a section has no items, omit it entirely +Group the milestone's issues into three buckets: +- **Done** — `state: closed`. +- **In progress** — open, and the issue number is referenced by some open PR body (`#N`, `closes #N`, `fixes #N`, `related to #N`, `issue #N`). This "referenced by an open PR" definition is the real rule — apply it exactly. +- **Not started** — open, and no open PR references it. -Do not post, comment, write files, or take any action. Output only. +Derive health badges for in-progress issues from their linked PR (same fields as personal mode). Report as a scannable summary titled with the milestone name and a closed/in-progress/not-started roll-up. Show assignees and URLs where they add value; omit empty buckets. **Do not post, comment, write files, or take any action. Output only.** --- -## Team mode (Steps T1–T3) - -Only entered when `--team` is detected in Step 1. - -### Step T1 — Fetch all open work (run both in parallel) +## Team mode (Steps T1–T2) +### T1 — Fetch (run both concurrently) ```bash gh pr list --state open --limit 100 \ --json number,title,url,author,labels,milestone,baseRefName,body,reviewDecision,statusCheckRollup,updatedAt,mergeable,isDraft -``` - -```bash gh issue list --state open --limit 200 \ --json number,title,url,assignees,labels,milestone,updatedAt ``` +If either fails, report and stop. -If either command fails, report the error and stop. - -### Step T2 — Group and classify - -Group items by person: -- PRs are grouped by `author.login` -- Issues are grouped by assignee (first assignee if multiple). Issues with no assignee go into an "Unassigned" group. - -Within each person's group, classify items the same way as personal mode (Step 4): -- **In progress** — open PRs (and linked issues) -- **Up next** — open issues not linked from any open PR - -Derive health badges (Step 4a) and flag stale items (Step 4b) for all items. - -### Step T3 — Output the team summary - -``` -## Team status - -<N> open PRs · <N> open issues · <N> people - -### @<person> (<N> in progress, <N> up next) -- #<number> <title> — PR <check badge> · <review badge>[ · <conflict badge>][ · <stale badge>][ [draft]] - <url> -- #<number> <title> [up next][ · <stale badge>] - -### @<person> (<N> in progress, <N> up next) -... - -### Unassigned (<N>) -- #<number> <title> - <url> - -### ⚠️ Stale -- #<number> <title> (@<person>) — last updated <date> (<N> days ago) -``` - -Rules: -- Sort people alphabetically by username -- Within each person, show in-progress items first, then up-next items -- Show health badges on PR items (same format as personal mode) -- Show `[draft]` on draft PRs -- Tag up-next items with `[up next]` for visual distinction -- "Unassigned" section appears at the bottom, only if there are unassigned issues -- "Stale" section consolidates all stale items across all people -- Omit any section or group with no items -- No "What's next" section in team mode +### T2 — Group and report -Do not post, comment, write files, or take any action. Output only. +Group by person: PRs by `author.login`; issues by first assignee (unassigned issues into an "Unassigned" group). Within each person, classify as personal mode does — **in progress** (open PRs and their linked issues) and **up next** (open issues not referenced by any open PR) — and derive health badges plus staleness. Report per-person sections with in-progress items first, an "Unassigned" section if any, and a consolidated stale section. **Do not post, comment, write files, or take any action. Output only.** --- ## Hard rules -- Never write to GitHub (no comments, labels, issue updates, or PR changes). -- 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. -<!-- generated by CodeCannon/sync.py | skill: status | adapter: claude | hash: 148f31a9 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +- Never write to GitHub (no comments, labels, issue updates, or PR changes) and never touch the working tree. Output only. +- If `gh` is unauthenticated or any fetch fails, report the error and stop immediately. Do not retry. +- Strip the leading `@` from the subject when passing to `gh` flags that reject it. +- `--team` is mutually exclusive with `--milestone`/`--sprint` and with a username. +- The `14` threshold is config-driven; `0` disables staleness. The "what's next" priority ordering is fixed — evaluate top to bottom, first match wins. +<!-- generated by CodeCannon/sync.py | skill: status | adapter: claude | hash: 99c9777e | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.codecannon.yaml b/.codecannon.yaml index af58bc0..8058b18 100644 --- a/.codecannon.yaml +++ b/.codecannon.yaml @@ -29,6 +29,7 @@ config: DEV_CMD: make dev ABANDON_CMD: make abandon CHECK_CMD: make check + TEST_CMD: make test MERGE_CMD: make merge DEPLOY_PREVIEW_CMD: make deploy-preview DEPLOY_PROD_CMD: make deploy-prod diff --git a/.cursor/rules/deploy.mdc b/.cursor/rules/deploy.mdc index cc923da..bf054a3 100644 --- a/.cursor/rules/deploy.mdc +++ b/.cursor/rules/deploy.mdc @@ -10,25 +10,24 @@ alwaysApply: false ## What `/deploy` does -`/deploy` is the final step in the workflow. It combines version bumping and release creation into a single command: check state, optionally bump the version, then create a GitHub Release (and in multi-branch mode, promote to production). +`/deploy` is the final step in the workflow. It combines version bumping and release creation into a single command: check state, optionally bump the version, then create a GitHub Release (and in multi-branch mode, promote the deploy branch to production first). + +The branching mode changes the shape of the release: in **trunk mode** (`BRANCH_PROD` only) `/deploy` tags and releases the current branch directly; in **multi-branch mode** (`BRANCH_DEV` set, optionally with `BRANCH_TEST`) it first opens and merges a release PR from the deploy branch into production, and that merge is what closes the linked issues. --- -## Step 1 — Verify branch +## Step 1 — Verify branch and sync -Run: -```bash -git branch --show-current -``` +Run `git branch --show-current`. The **deploy branch** for this project is: -Required branch: `dev` (two-branch mode). +`dev` (two-branch mode). -If not on the required branch, abort and say: "Switch to `<required-branch>` before running `/deploy`." +If not on the deploy branch, abort: "Switch to `<deploy-branch>` before running `/deploy`." -Sync to the remote before proceeding. The script below guards against uncommitted local changes, then runs `git checkout`, `git fetch`, and `git reset --hard origin/<base>` as one atomic operation. The deploy branch is never edited locally under the CodeCannon workflow (only fast-forwarded from CodeCannon's own merges), so the hard reset is the correct sync; the dirty-tree guard catches accidental local edits before they get silently discarded. +Then sync it to the remote. The script guards against uncommitted local changes, then runs `git checkout`, `git fetch`, and `git reset --hard origin/<deploy-branch>` as one atomic operation. The deploy branch is never edited locally under the CodeCannon workflow (only fast-forwarded from merges), so the hard reset is the correct sync; the dirty-tree guard catches accidental local edits before they are silently discarded. ```bash -python3 CodeCannon/skills/github-agile/scripts/sync-base-branch.py dev +python3 CodeCannon/skills/github-agile/scripts/sync-base-branch.py <deploy-branch> ``` If the script exits non-zero, stop and resolve the issue it reports before continuing. @@ -37,87 +36,35 @@ If the script exits non-zero, stop and resolve the issue it reports before conti ## Step 2 — Check current state -### Find the latest version tag - -```bash -git describe --tags --abbrev=0 -``` +Find the latest version tag (`git describe --tags --abbrev=0`; if none, note this is the first release) and read the current version with `cat VERSION`. -If no tag exists, note this is the first release. - -### Read current version +Show the merge commits (and their PRs) since the last tag. The range depends on the mode: ```bash -cat VERSION +git log main..<deploy-branch> --merges --pretty=format:"%s" ``` -### Show commits since last tag +Merge-commit subjects have the form `Merge pull request #N from branch/name` — parse the PR numbers, then retrieve each body with `gh pr view <N> --json number,title,body`. -If a previous tag exists, show what's on the branch since that tag: +From those PR bodies, compile the release's issue links: -```bash -git log main..dev --merges --pretty=format:"%s" -``` - -Parse PR numbers from merge commit subjects (format: `Merge pull request #N from branch/name`). +Keep closing keywords and context references **separate — do not merge them into one set**: -For each PR number found, retrieve the PR body: -```bash -gh pr view <N> --json number,title,body -``` +- **Close set** — the union of every `Closes #N` line across all constituent PR bodies. These auto-close when the release PR merges into `main`. Record, per constituent PR, the exact `Closes #N` lines so they can be reproduced verbatim in the release PR body. +- **Reference set** — issues mentioned only via `Related to #N` or the legacy `Issue #N` form. These are context links and will **not** close. Legacy `Issue #N` carries no recoverable close-intent, so it stays in the reference set rather than being guessed into the close set; the human gate surfaces it so you can manually close any straggler that should have closed. +- **PRs included** (number + title). -Extract closing keywords **separately** from context references — do **not** merge them into a single set: +Also check for open unmerged PRs (`gh pr list --state open --json number,title,headRefName`). -- **Close set** — the union of every `Closes #N` line across all constituent PR bodies. These issues will auto-close when the release PR merges into `main`. Record, per constituent PR, the exact `Closes #N` lines it contained so they can be reproduced verbatim in the release PR body. -- **Reference set** — issues mentioned only via `Related to #N`, or via the legacy `Issue #N` form. These are context links and will **not** close. Legacy `Issue #N` carries no recoverable close-intent, so it stays in the reference set rather than being guessed into the close set; the HUMAN GATE surfaces it so you can manually close any straggler that should have been a `Closes`. - -Compile: -- List of PRs included (number + title) -- Close set and reference set, kept distinct - -### Check for open unmerged PRs - -```bash -gh pr list --state open --json number,title,headRefName -``` - -### Present the summary - -Tell the user: - -``` -Current version: X.Y.Z -Latest tag: vX.Y.Z - -Commits/PRs since last tag: - #17 — Add /docs directory - #18 — Fix checkout runtime error - -Open PRs not yet merged: - #19 — Add dark mode (feature/dark-mode) - -Would you like to bump the version before deploying? - - **patch** → X.Y.C - - **minor** → X.B.0 - - **major** → A.0.0 - - **specific** → enter a version number - - **skip** → proceed to release with the latest existing tag -``` - -Wait for their response. +Present a summary — current version, latest tag, the PRs/issues since that tag, any open PRs — and ask whether to bump the version before deploying (patch → X.Y.C, minor → X.B.0, major → A.0.0, a specific version, or skip to release the latest existing tag). Wait for their response. --- ## Step 3 — Version bump (if requested) -If the user chose to skip, find the latest version tag in the branch history: -```bash -git describe --tags --abbrev=0 -``` - -If no tag is found at all (first release), warn: "No version tag found. You must bump the version before deploying." Return to the version bump prompt. Otherwise, use the tag found as the release version. +If the user chose **skip**, use the latest existing tag (`git describe --tags --abbrev=0`) as the release version. If none exists (first release), warn "No version tag found. You must bump the version before deploying." and return to the bump prompt. -If the user chose a bump level, map their response to a bump command and run `bump-and-tag.py`, which performs the bump, verifies the resulting tag (creating an annotated fallback if `tag.forceSignAnnotated` silently rejected a lightweight tag), and pushes both the commit and the tag. The resolved version is printed on stdout — capture it for the release step. +If the user chose a bump level, map it to a bump command and run `bump-and-tag.py`, which performs the bump, verifies the resulting tag (creating an annotated fallback if `tag.forceSignAnnotated` silently rejected a lightweight tag), and pushes both the commit and the tag. The resolved version is printed on stdout — capture it as `<new-version>`. | User says | `--bump-cmd` | |---|---| @@ -132,59 +79,37 @@ python3 CodeCannon/skills/github-agile/scripts/bump-and-tag.py \ --version-read-cmd "cat VERSION" ``` -If the script exits non-zero, stop and resolve the issue it reports before continuing. On success, the version printed on stdout is the new version — use it as `<new-version>` in subsequent steps. +If the script exits non-zero, stop and resolve the issue it reports before continuing. --- ## Step 4 — Compute release contents -Determine the version tag (either from the bump just performed, or from the existing HEAD tag if the user skipped bumping). +Determine the release version tag (from the bump just performed, or the existing HEAD tag if the user skipped). Find the previous tag for the changelog range: `git describe --abbrev=0 <version-tag>^`. -Find the previous tag to determine the range: -```bash -git describe --abbrev=0 <version-tag>^ -``` - -Use the PR list, close set, and reference set already computed in Step 2. If the version bump added new commits, re-fetch if needed. +Reuse the PR list, close set, and reference set already computed in Step 2. If the version bump added new commits, re-fetch as needed. --- ## Step 5 — HUMAN GATE -Show the user the release summary. Example format: - -``` -Ready to release vX.Y.Z to production. +Show the release summary — the target version, the PRs included, and the issue links: -PRs included: - #17 — Add /docs directory - #18 — Fix checkout runtime error +- Issues that will **close** on merge (the close set, reproduced verbatim from constituent PRs). +- Issues **referenced but not closing** (the reference set — confirm none of these should actually close). -Issues that will close on merge (Closes #N, reproduced verbatim from constituent PRs): - #14 — Add /docs directory - #15 — Fix checkout runtime error - -Issues referenced but NOT closing (Related to #N / legacy Issue #N — confirm none of these should actually close): - #20 — Tighten error copy on the upload form - -Have you tested all of the above on preview? Type 'release' to confirm. -``` +Confirm the deploy branch has been tested: +"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. +Wait for "release" or an explicit confirmation. Any other response → stop and ask what they'd like to change. --- -## Step 6 — Create PR: `dev` → `main` +## Step 6 — Promote: `<deploy-branch>` → `main` -First, create a temp directory for this invocation: +Create a temp directory for this invocation (`python3 CodeCannon/skills/github-agile/scripts/make-workdir.py`) and note the returned path — use it for all temp files here. -```bash -python3 CodeCannon/skills/github-agile/scripts/make-workdir.py -``` - -Note the returned path (e.g. `/tmp/CodeCannon/a8f3b2`). Use this path for all temp files in this invocation. - -Then use your file-writing tool (not Bash) to create `<tmpdir>/release_pr_body.md`: +Use your file-writing tool (not Bash) to create `<tmpdir>/release_pr_body.md`: ```markdown Release vX.Y.Z @@ -199,53 +124,31 @@ Closes #15 Related to #20 ``` -Reproduce **every** `Closes #N` line from the close set computed in Step 2 — verbatim, one per line, omitting none. Add a `Related to #N` line for each issue in the reference set so the links appear on the release PR without triggering an auto-close. If the reference set is empty, omit the `Related to` lines entirely. +Reproduce **every** `Closes #N` line from the close set — verbatim, one per line, omitting none. Add a `Related to #N` line for each issue in the reference set so the links appear without triggering an auto-close; if the reference set is empty, omit the `Related to` lines entirely. -Then create the PR (do NOT use `--body`, `--body-file -`, or heredocs): +Create the PR (do NOT use `--body`, `--body-file -`, or heredocs), with `--head` set to the deploy branch: ```bash -gh pr create --base main --head dev \ +gh pr create --base main --head <deploy-branch> \ --title "Release vX.Y.Z" \ --body-file <tmpdir>/release_pr_body.md ``` -Note the PR number from the output. - -The `Closes #N` lines will auto-close the linked issues because this PR merges into `main` (the default branch). - -> **Critical:** Use the unqualified `#N` form only. Never write `Closes owner/repo#N`, even for same-repo refs — GitHub's closing-keyword parser only populates `closingIssuesReferences` for the unqualified form, and the qualified form silently breaks auto-close. - ---- - -## Step 7 — Merge +> **Critical:** Use the unqualified `#N` form only. Never write `Closes owner/repo#N`, even for same-repo refs — GitHub's closing-keyword parser only populates `closingIssuesReferences` for the unqualified form, and the qualified form silently breaks auto-close. The `Closes #N` lines auto-close the linked issues because this PR merges into `main` (the default branch). -Do NOT use `make merge` — it refuses PRs targeting `main`. Use `gh pr merge` directly: - -```bash -gh pr merge <pr-number> --merge -``` +Then merge. Do NOT use `make merge` — it refuses PRs targeting `main`. Use `gh pr merge <pr-number> --merge` directly. --- -## Step 8 — Create GitHub Release +## Step 7 — Create the GitHub Release -**Publish confirmation — required before writing the release notes or creating the Release.** The GitHub Release is a public-surface action. The single word from Step 5 authorized the promotion/merge (already done); the public publish gets its own explicit confirmation. Tell the user (substitute the actual tag, e.g. `v0.13.0`): +**Publish confirmation — required before writing the release notes or creating the Release.** The GitHub Release is a public-surface action; the confirmation from Step 5 authorized the promotion, but the public publish gets its own explicit confirmation. Tell the user (substitute the actual tag, e.g. `v0.13.0`): > Publishing GitHub Release `<version-tag>` — the final public step. Confirm by pasting: `publish <version-tag>` Wait for the user to paste `publish <version-tag>` (or an explicit version-named variant such as `ship <version-tag>`). Any other response → stop and ask what they'd like to change. The version-named phrase is deliberate: Claude Code's auto-mode safety classifier requires authorization that names the release before `gh release create` runs, so the generic Step 5 confirmation is not relied on for the public publish. If a harness still blocks the call after this confirmation (e.g. an older client), the user can re-confirm with `publish <version-tag> release` to unblock. ---- - -The version tag (from Step 3) and the PR/issue list (from Step 4) are already known. Find the previous tag to build the changelog link: - -```bash -git describe --abbrev=0 <version-tag>^ -``` - -If no previous tag exists, omit the "Full changelog" line. - -Use your file-writing tool (not Bash) to create `<tmpdir>/release_notes.md` (same temp directory from Step 6): +The version tag and PR/issue list are already known; the previous tag comes from Step 4 (if there is no previous tag, omit the "Full changelog" line). Create a temp directory if you haven't already (`python3 CodeCannon/skills/github-agile/scripts/make-workdir.py`), then use your file-writing tool (not Bash) to create `<tmpdir>/release_notes.md`: ```markdown ## Changes @@ -256,7 +159,7 @@ Use your file-writing tool (not Bash) to create `<tmpdir>/release_notes.md` (sam **Full changelog:** https://github.com/<owner>/<repo>/compare/<previous-tag>...<version-tag> ``` -Then create the release (do NOT use `--notes`, `--notes-file -`, or heredocs): +Format each PR line as `- #<linked-issue> — <PR title> (PR #<N>)`; if a PR had no linked issue, use just the PR title. Then create the release (do NOT use `--notes`, `--notes-file -`, or heredocs): ```bash gh release create <version-tag> \ @@ -264,15 +167,11 @@ gh release create <version-tag> \ --notes-file <tmpdir>/release_notes.md ``` -Format each PR line as `- #<linked-issue> — <PR title> (PR #<N>)`. If a PR had no linked issue, omit the `#<issue>` prefix and use just the PR title. - -After the command runs, note the release URL from the output. +Note the release URL from the output. --- -## Step 9 — Report - -Tell the user: +## Step 8 — Report -> "Released vX.Y.Z. Issues #N, #M closed automatically. GitHub Release vX.Y.Z created at `<url>`. Run `make deploy-prod` to ship to production." -<!-- generated by CodeCannon/sync.py | skill: deploy | adapter: cursor | hash: cf532b25 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +Tell the user: "Released vX.Y.Z. Linked issues are closed. GitHub Release vX.Y.Z created at `<url>`. Run `make deploy-prod` to ship to production." +<!-- generated by CodeCannon/sync.py | skill: deploy | adapter: cursor | hash: ab16e157 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.cursor/rules/start.mdc b/.cursor/rules/start.mdc index 7708419..f6f451c 100644 --- a/.cursor/rules/start.mdc +++ b/.cursor/rules/start.mdc @@ -28,12 +28,10 @@ Otherwise → go to **Case A: New work**. > **Execution order:** Resolve labels and milestones **now**, before entering Case A Step 1. If milestone auto-detection requires a user prompt (2+ open milestones), that prompt happens here — not later during issue creation. By the time you reach Step 2's human gate, all metadata must already be resolved so that Step 3 can proceed without re-prompting. -The argument string may contain optional inline flags after the description. Parse as follows: +The description may be followed by optional flags — `--label`/`-l` and `--milestone`/`-m`, in any order. Separate the description from the flags yourself; the flags carry these meanings: -1. **Identify flags** — scan for the first token that starts with `--label`, `-l`, `--milestone`, or `-m`. Everything before it is the **description**. Everything from the first flag onward is **flags**. -2. **`--label <value>` / `-l <value>`** — comma-separated label string (e.g. `bug` or `enhancement,ux`). If provided, it **bypasses label auto-selection entirely** for this invocation — use the value verbatim. Labels containing spaces must be quoted (e.g. `--label "good first issue"`). -3. **`--milestone <value>` / `-m <value>`** — milestone name or number (e.g. `Sprint 4` or `12`). Pass the value as-is; GitHub accepts both names and numbers. -4. **Flags may appear in any order** after the description. +- **`--label <value>` / `-l <value>`** — a comma-separated label string used **verbatim**, bypassing label auto-selection entirely for this invocation. Quote values containing spaces (e.g. `--label "good first issue"`). +- **`--milestone <value>` / `-m <value>`** — a milestone name or number (GitHub accepts both names and numbers). **Label resolution (three-tier, Case A only):** @@ -58,14 +56,6 @@ After parsing flags, determine the active milestone in this order: - **1 result** → use its title silently. Inform the user inline: `(milestone: <title>)`. - **2+ results** → show the numbered list, ask once: **"Multiple open milestones — which should this issue go under? (enter a number or title, or 'none')"**. Accept milestone number, title, or "none"/"skip". Wait for response before continuing. -**Examples:** - -| `$ARGUMENTS` | Description | Labels | Milestone | -|---|---|---|---| -| `Add dark mode toggle to settings page` | `Add dark mode toggle to settings page` | auto-selected from pool | auto-detected | -| `Add dark mode --label enhancement` | `Add dark mode` | `enhancement` (verbatim) | auto-detected | -| `Add dark mode --label enhancement,ux --milestone "Sprint 4"` | `Add dark mode` | `enhancement,ux` (verbatim) | `Sprint 4` | - > Replace vs append: flags **replace** auto-selection entirely, they do not append. This avoids silent label duplication and milestone conflicts. --- @@ -103,7 +93,7 @@ If on any other branch → proceed to Case A or Case B as determined by the `$AR ### Step 1 — Investigate -Read the relevant code. Propose a concrete implementation approach. Be specific about which files change and how. +Read the relevant code using your harness's native file-reading and search tools (read, grep/glob, and the like) rather than shell pipelines — hand-rolled `find … | xargs`, `grep ; awk`, or redirection shapes trigger permission prompts that cannot be permanently allowed. Then propose a concrete implementation approach, specific about which files change and how. ### Step 2 — HUMAN GATE @@ -223,7 +213,14 @@ Show the user: `On branch feature/<name>` ### Step 5 — Write the code -Now write the code. Do NOT commit anything. +Write the code using your harness's native editing tools. Do NOT commit anything. +To exercise your work as you go, run the project's configured test command rather than hand-rolling a test invocation — a hand-built `python3 -m unittest … > /tmp/…` or similar redirection shape triggers permission prompts that a configured command avoids: + +```bash +make test +``` + +If it fails because the target does not exist, tell the user rather than improvising a replacement. When done, say: **"When you've verified locally, reply `yes` to submit, or say what to change."** @@ -318,7 +315,14 @@ git branch --show-current ### Step 5 — Write the code -Continue from where work left off. Do NOT commit. +Continue from where work left off, using your harness's native editing tools. Do NOT commit. +To exercise your work as you go, run the project's configured test command rather than hand-rolling a test invocation — a hand-built `python3 -m unittest … > /tmp/…` or similar redirection shape triggers permission prompts that a configured command avoids: + +```bash +make test +``` + +If it fails because the target does not exist, tell the user rather than improvising a replacement. When done, say: **"When you've verified locally, reply `yes` to submit, or say what to change."** @@ -337,4 +341,4 @@ When done, say: **"When you've verified locally, reply `yes` to submit, or say w - 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 <number> --remove-assignee @me`. - Apply resolved labels and milestone to every new issue. Label resolution order: per-invocation flag → pool selection from `bug, documentation, enhancement, chore` → omit `--label` entirely. Never apply a label outside `bug, documentation, enhancement, chore`. - Milestone resolution order: per-invocation flag → auto-detected from GitHub open milestones. Never prompt for a milestone more than once per invocation. -<!-- generated by CodeCannon/sync.py | skill: start | adapter: cursor | hash: de29216a | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: start | adapter: cursor | hash: 6606010e | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.cursor/rules/status.mdc b/.cursor/rules/status.mdc index 4111290..6649f1b 100644 --- a/.cursor/rules/status.mdc +++ b/.cursor/rules/status.mdc @@ -8,45 +8,39 @@ alwaysApply: false --- -## Step 1 — Parse arguments +## What `/status` does -First, check whether `$ARGUMENTS` contains `--milestone`, `--sprint`, or `--team`. +`/status` prints a read-only, standup-ready snapshot of in-progress and recently completed work, then a single "what's next" suggestion. It never writes to GitHub or the working tree — it only reads and reports. -**Milestone mode:** If `--milestone` or `--sprint` is present, extract everything after the flag as the milestone name (trim leading/trailing whitespace; preserve internal spaces). Ignore any other arguments. Enter milestone mode (Steps M1–M3 below) and skip Steps 2–6. +Because it is read-only, the *shape* of its output does not matter: a differently-formatted-but-accurate summary is a fine result. Derive a clear, scannable layout yourself. What this skill pins down is the data to fetch, how to classify it, and the one piece of real opinion — the "what's next" ordering. -Examples: -- `--milestone Sprint 4` → milestone name = `Sprint 4` -- `--sprint Sprint 4` → milestone name = `Sprint 4` -- `--milestone Q2 Release` → milestone name = `Q2 Release` -- `--milestone 12` → milestone name = `12` - -**Team mode:** If `--team` is present, enter team mode (Steps T1–T3 below) and skip Steps 2–6. `--team` is mutually exclusive with `--milestone`/`--sprint` and username arguments. If both are present, report the conflict and stop. +--- -**Personal mode** (no `--milestone` / `--sprint` / `--team` flag): determine: +## Step 1 — Determine mode -- **subject**: default `@me`. If the argument starts with `@` or is a plain word that is not a number, treat it as a GitHub username. Strip the leading `@` for `gh` commands that do not accept it (e.g. `gh pr list --author alice`); keep it for display. -- **lookback**: default `7`. If the argument is a number (digits only), use it as the lookback window in days. +Three mutually exclusive modes, selected from `$ARGUMENTS`: -No argument → subject = `@me`, lookback = `7`. +- **Milestone mode** — `--milestone` or `--sprint` is present. Everything after the flag is the milestone name (a name or a number; trim outer whitespace, preserve internal spaces). Ignore other arguments. Run Steps M1–M2. +- **Team mode** — `--team` is present. Run Steps T1–T2. `--team` is mutually exclusive with `--milestone`/`--sprint` and with a username; if combined, report the conflict and stop. +- **Personal mode** — no mode flag. **Subject** defaults to `@me`; a `@name` or a non-numeric word is a username (strip the leading `@` for `gh` flags that reject it, keep it for display). **Lookback** defaults to `7`; a bare number is the lookback in days. --- -## Step 2 — Fetch GitHub data (run all in parallel) +## Step 2 — Fetch GitHub data (personal mode) -Run these commands concurrently: +Run these concurrently. If any `gh` command exits non-zero (including auth errors), report the message and stop — do not retry. -**Open PRs authored by subject:** +**Open PRs authored by subject** — request enough fields to derive health (draft, CI, review decision, merge conflict) and staleness: ```bash gh pr list --author <subject> --state open \ --json number,title,url,labels,milestone,baseRefName,body,reviewDecision,statusCheckRollup,updatedAt,mergeable,isDraft ``` -**Recently merged PRs (last `<lookback>` days):** +**Recently merged PRs**, filtered to those merged within `<lookback>` days: ```bash gh pr list --author <subject> --state merged --limit 20 \ --json number,title,url,mergedAt,labels,baseRefName ``` -Filter the results to keep only entries where `mergedAt` is within the last `<lookback>` days. **Open issues assigned to subject:** ```bash @@ -54,317 +48,103 @@ gh issue list --assignee <subject> --state open \ --json number,title,url,labels,milestone,updatedAt ``` -**PRs requesting your review** (only when subject is `@me`): +**PRs requesting your review** — only when subject is `@me`; skip for other users: ```bash gh pr list --search "review-requested:@me" --state open \ --json number,title,url,author,updatedAt ``` -Skip this query when viewing another user's status. -If any `gh` command exits with a non-zero status (including auth errors), report the error message and stop. Do not retry. - ---- - -## Step 3 — Fetch local git context - -Check if the current directory is inside a git repository: -```bash -git rev-parse --is-inside-work-tree -``` - -If yes, run: +Also fetch local git context (skip and note if not in a git repo — `git rev-parse --is-inside-work-tree`): ```bash git log --oneline --since="<lookback> days ago" ``` -If not inside a git repo, skip this step and note it was skipped in the output. - --- -## Step 4 — Classify items - -Using the data from Steps 2 and 3, classify each item: - -- **In progress** — open PRs. For each, attempt to identify a linked issue number from the PR body (look for `#N`, `closes #N`, `fixes #N`, `issue #N`). If found, cross-reference with open issues. -- **Done** — merged PRs within the lookback window. -- **Up next** — open issues that are NOT associated with any open PR (i.e. no open PR body references their issue number). -- **Needs your review** — PRs from the review-requested query (only when subject is `@me`). - -An open issue that IS linked from an open PR body appears under "In progress" alongside that PR, not under "Up next". +## Step 3 — Classify and report (personal mode) -### 4a — Derive health badges +Sort items into these buckets: -For each open PR, derive the following badges: +- **In progress** — open PRs. Identify a linked issue from the PR body (`#N`, `closes #N`, `fixes #N`, `issue #N`) and cross-reference open issues. +- **Done** — PRs merged within the lookback window. +- **Up next** — open issues whose number is **not** referenced by any open PR body. (An issue linked from an open PR belongs under *In progress* with that PR, not here.) +- **Needs your review** — the review-requested query (only when subject is `@me`). -**Draft status:** -- If `isDraft` is `true` → `[draft]` +For each open PR, derive health from the JSON — draft state, CI status from `statusCheckRollup`, review state from `reviewDecision`, merge conflict from `mergeable`. Present each as a compact badge; omit a badge when it does not apply or is not configured. -**CI check status** (from `statusCheckRollup`): -- All checks have `status: COMPLETED` and `conclusion: SUCCESS` → `✅ checks passing` -- Any check has `conclusion: FAILURE` → `❌ checks failing` -- Checks are still running or have other states → `⏳ checks pending` -- No checks configured → omit badge +**Staleness:** flag any open PR or issue not updated within `14` days (a threshold of `0` disables staleness entirely). Note the last-updated date and age. This is a real config-driven rule — honor the threshold exactly. -**Review decision** (from `reviewDecision`): -- `APPROVED` → `✅ approved` -- `CHANGES_REQUESTED` → `🔄 changes requested` -- `REVIEW_REQUIRED` or empty → `⏳ awaiting review` +Report the buckets as a scannable summary: a heading naming the subject and lookback, a one-line count roll-up, then a section per non-empty bucket, then the local commits (or a note that git was skipped). Show labels/milestone only when present; dates as `YYYY-MM-DD`. If every GitHub bucket is empty, say so plainly for the subject and window. -**Merge conflict** (from `mergeable`): -- `CONFLICTING` → `⚠️ conflicts` -- `MERGEABLE` or `UNKNOWN` → omit badge - -### 4b — Flag stale items - -For each open PR and open issue, check `updatedAt`. If the item has not been updated within `14` days (default: 14; disabled when set to 0), flag it as stale. Record the last-updated date and the number of days since the last update. - -A stale item gets an inline `⚠️ stale (<N>d)` badge appended after any other badges. +**Do not post, comment, write files, or take any action. Output only.** --- -## Step 5 — Output the summary - -Print a formatted summary. Use this structure: - -``` -## Status for <subject> — last <lookback> days - -<N> in progress · <N> done · <N> up next[ · <N> need your review] - -### In progress -- #<number> <title> [<labels>] [<milestone>] [draft] - PR: <url> · <check badge> · <review badge>[ · <conflict badge>][ · <stale badge>] - Linked issue: #<number> (if found) +## Step 4 — What's next (personal mode) -### Done -- #<number> <title> [<labels>] — merged <date> - PR: <url> +After the summary, append **one** actionable suggestion. Gather the extra local state you need (current branch, `git status --porcelain`, latest tag via `git describe --tags --abbrev=0`, unreleased commit count via `git rev-list <tag>..HEAD --count`, and the current branch's PR review/check state via `gh pr view` — treat a non-zero exit as "no PR for this branch"). Skip git lookups when not in a repo. -### Needs your review -- #<number> <title> (by @<author>) - PR: <url> +Evaluate these conditions **in order** and use the **first** match. This ordering is the workflow's opinion about what the operator should do next — it is load-bearing, not formatting: -### Up next -- #<number> <title> [<labels>] [<milestone>][ · <stale badge>] - Issue: <url> +| Priority | Condition | Suggestion | +|----------|-----------|------------| +| 1 | On a `feature/*` branch with uncommitted changes | You have uncommitted changes on `<branch>`. When ready, run `/submit-for-review`. | +| 2 | On a `feature/*` branch, open PR is `APPROVED` and all checks `COMPLETED` | PR #<number> is approved and checks pass. Consider running `/deploy`. | +| 3 | On a `feature/*` branch with an open PR in any other state | PR #<number> (<title>) is open and awaiting review. | +| 3.5 | Subject is `@me` and PRs request your review | Append to the current suggestion (or stand alone if nothing higher matched): You also have <N> PR(s) awaiting your review. | +| 4 | On a `feature/*` branch, no open PR, clean tree | No open PR for `<branch>`. Run `/submit-for-review` to open one. | +| 5 | On the integration branch with unreleased commits since the last tag | <N> commit(s) on `<branch>` since `<tag>`. Run `/deploy` when ready to release. | +| 6 | No open PRs and no open issues assigned to subject | Nothing in progress. Run `/start` to begin new work. | +| 7 | Open issues exist in "Up next" | Next up is #<number> (<title>). Run `/start <number>` to pick it up. | -### ⚠️ Stale -- #<number> <title> — last updated <date> (<N> days ago) +If none match, omit the "what's next" section. Omit it entirely in milestone mode. --- -Local commits (current branch): -<git log output, or "skipped — not in a git repo"> -``` -Rules: -- **Summary counts line**: show immediately after the heading. Omit zero-count segments (e.g., if nothing is done, skip that segment). "need your review" only appears when subject is `@me` and the count is > 0. -- **Health badges**: show on the second line of each "In progress" item, after the PR URL, separated by ` · `. Omit individual badges that don't apply (e.g., no conflict badge if mergeable). -- **Draft badge**: show `[draft]` inline in the first line of draft PRs, before any other badges. -- **Stale section**: a dedicated section at the bottom (before "Local commits") listing all stale items from any section, with their last-updated date and age. This gives a consolidated view. Individual items also get the inline `⚠️ stale (<N>d)` badge in their own sections. -- **"Needs your review" section**: only shown when subject is `@me` and there are PRs requesting review. Placed between "Done" and "Up next". -- Omit any section that has no items — do not show an empty heading. -- Show labels only if present; show milestone only if present. -- Dates use `YYYY-MM-DD` format. -- If all GitHub sections are empty, print: `Nothing found for <subject> in the last <lookback> days.` - -Do not post, comment, write files, or take any action. Output only. - ---- - -## Step 6 — What's next - -After the status summary, append a single actionable suggestion based on local git state and the GitHub data already fetched. - -### 6a — Gather additional local state - -Run these commands (skip if not in a git repo): - -```bash -git branch --show-current -``` - -```bash -git status --porcelain -``` - -```bash -git describe --tags --abbrev=0 -``` - -```bash -git rev-list <latest-tag>..HEAD --count -``` - -From the GitHub data fetched in Step 2, also check for the current branch's PR approval status: - -```bash -gh pr view --json number,title,url,reviewDecision,statusCheckRollup \ - --jq '{number,title,url,reviewDecision,checks: [.statusCheckRollup[]? | .status]}' -``` - -If `gh pr view` exits non-zero (no PR for current branch), note that there is no open PR. - -### 6b — Determine suggestion - -Evaluate the following conditions **in order**. Use the **first** match: - -| Priority | Condition | Output | -|----------|-----------|--------| -| 1 | On a `feature/*` branch with uncommitted changes (`git status --porcelain` is non-empty) | `What's next: You have uncommitted changes on \`<branch>\`. When ready, run \`/submit-for-review\`.` | -| 2 | On a `feature/*` branch with an open PR that has `reviewDecision: APPROVED` and all status checks are `COMPLETED` | `What's next: PR #<number> is approved and checks pass. Consider running \`/deploy\`.` | -| 3 | On a `feature/*` branch with an open PR (any other review/check state) | `What's next: PR #<number> (<title>) is open and awaiting review.` | -| 3.5 | Subject is `@me` and there are PRs requesting your review (from Step 2 query) | Append to the current suggestion (or show standalone if no higher priority matched): `You also have <N> PR(s) awaiting your review.` | -| 4 | On a `feature/*` branch with no open PR and clean working tree | `What's next: No open PR for \`<branch>\`. Run \`/submit-for-review\` to open one.` | -| 5 | On the integration branch (`dev`, `develop`, or `main` when no integration branch exists) with unreleased commits (rev-list count > 0 since last tag) | `What's next: <N> commit(s) on \`<branch>\` since \`<tag>\`. Run \`/deploy\` when ready to release.` | -| 6 | No open PRs, no open issues assigned to subject | `What's next: Nothing in progress. Run \`/start\` to begin new work.` | -| 7 | Open issues exist in "Up next" | `What's next: Next up is #<number> (<title>). Run \`/start <number>\` to pick it up.` | - -If none of the above match, omit the "What's next" section entirely. - -### 6c — Format - -Print the suggestion after a horizontal rule, below the local commits section: - -``` ---- -🧭 <suggestion text> -``` - -This section is omitted in milestone mode. - ---- - -## Milestone mode (Steps M1–M3) - -Only entered when `--milestone` or `--sprint` is detected in Step 1. - -### Step M1 — Fetch milestone issues +## Milestone mode (Steps M1–M2) +### M1 — Fetch ```bash gh issue list --milestone "<name>" --state all --limit 200 \ --json number,title,state,labels,assignees,url -``` - -If this command fails for any reason (milestone not found, auth error, etc.), report the error and stop. - -### Step M2 — Classify issues - -Fetch all open PRs to detect which issues are in progress (with health fields): - -```bash gh pr list --state open \ --json number,title,body,baseRefName,reviewDecision,statusCheckRollup,mergeable,isDraft ``` +If the issue query fails (milestone not found, auth error), report and stop. -Group issues into three buckets: - -- **Done** — `state: closed` -- **In progress** — `state: open` AND the issue number appears in any open PR body (look for `#<number>`, `closes #<number>`, `fixes #<number>`, `related to #<number>`, `issue #<number>`) -- **Not started** — `state: open` AND no open PR body references the issue number - -For in-progress issues, derive health badges from the linked PR using the same rules as Step 4a (check status, review decision, draft, conflict). - -### Step M3 — Output the summary - -``` -## Sprint: <name> - -<Y> of <total> issues closed · <Z> in progress · <W> not started - -### In progress (<Z>) -- #<number> <title> [@<assignee>] [<milestone>][ [draft]] - <url> · <check badge> · <review badge>[ · <conflict badge>] - -### Not started (<W>) -- #<number> <title> [@<assignee>] +### M2 — Classify and report -### Done (<Y>) -- #<number> <title> -``` - -Rules: -- Show "In progress" first, then "Not started", then "Done" -- Show assignee only if present; omit if unassigned -- Show URLs only for in-progress items; omit URLs for closed issues -- Show health badges on in-progress items (same derivation as Step 4a) -- If a section has no items, omit it entirely +Group the milestone's issues into three buckets: +- **Done** — `state: closed`. +- **In progress** — open, and the issue number is referenced by some open PR body (`#N`, `closes #N`, `fixes #N`, `related to #N`, `issue #N`). This "referenced by an open PR" definition is the real rule — apply it exactly. +- **Not started** — open, and no open PR references it. -Do not post, comment, write files, or take any action. Output only. +Derive health badges for in-progress issues from their linked PR (same fields as personal mode). Report as a scannable summary titled with the milestone name and a closed/in-progress/not-started roll-up. Show assignees and URLs where they add value; omit empty buckets. **Do not post, comment, write files, or take any action. Output only.** --- -## Team mode (Steps T1–T3) - -Only entered when `--team` is detected in Step 1. - -### Step T1 — Fetch all open work (run both in parallel) +## Team mode (Steps T1–T2) +### T1 — Fetch (run both concurrently) ```bash gh pr list --state open --limit 100 \ --json number,title,url,author,labels,milestone,baseRefName,body,reviewDecision,statusCheckRollup,updatedAt,mergeable,isDraft -``` - -```bash gh issue list --state open --limit 200 \ --json number,title,url,assignees,labels,milestone,updatedAt ``` +If either fails, report and stop. -If either command fails, report the error and stop. - -### Step T2 — Group and classify - -Group items by person: -- PRs are grouped by `author.login` -- Issues are grouped by assignee (first assignee if multiple). Issues with no assignee go into an "Unassigned" group. - -Within each person's group, classify items the same way as personal mode (Step 4): -- **In progress** — open PRs (and linked issues) -- **Up next** — open issues not linked from any open PR - -Derive health badges (Step 4a) and flag stale items (Step 4b) for all items. - -### Step T3 — Output the team summary - -``` -## Team status - -<N> open PRs · <N> open issues · <N> people - -### @<person> (<N> in progress, <N> up next) -- #<number> <title> — PR <check badge> · <review badge>[ · <conflict badge>][ · <stale badge>][ [draft]] - <url> -- #<number> <title> [up next][ · <stale badge>] - -### @<person> (<N> in progress, <N> up next) -... - -### Unassigned (<N>) -- #<number> <title> - <url> - -### ⚠️ Stale -- #<number> <title> (@<person>) — last updated <date> (<N> days ago) -``` - -Rules: -- Sort people alphabetically by username -- Within each person, show in-progress items first, then up-next items -- Show health badges on PR items (same format as personal mode) -- Show `[draft]` on draft PRs -- Tag up-next items with `[up next]` for visual distinction -- "Unassigned" section appears at the bottom, only if there are unassigned issues -- "Stale" section consolidates all stale items across all people -- Omit any section or group with no items -- No "What's next" section in team mode +### T2 — Group and report -Do not post, comment, write files, or take any action. Output only. +Group by person: PRs by `author.login`; issues by first assignee (unassigned issues into an "Unassigned" group). Within each person, classify as personal mode does — **in progress** (open PRs and their linked issues) and **up next** (open issues not referenced by any open PR) — and derive health badges plus staleness. Report per-person sections with in-progress items first, an "Unassigned" section if any, and a consolidated stale section. **Do not post, comment, write files, or take any action. Output only.** --- ## Hard rules -- Never write to GitHub (no comments, labels, issue updates, or PR changes). -- 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. -<!-- generated by CodeCannon/sync.py | skill: status | adapter: cursor | hash: 86d1632a | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +- Never write to GitHub (no comments, labels, issue updates, or PR changes) and never touch the working tree. Output only. +- If `gh` is unauthenticated or any fetch fails, report the error and stop immediately. Do not retry. +- Strip the leading `@` from the subject when passing to `gh` flags that reject it. +- `--team` is mutually exclusive with `--milestone`/`--sprint` and with a username. +- The `14` threshold is config-driven; `0` disables staleness. The "what's next" priority ordering is fixed — evaluate top to bottom, first match wins. +<!-- generated by CodeCannon/sync.py | skill: status | adapter: cursor | hash: c0de60d9 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.gemini/skills/deploy/SKILL.md b/.gemini/skills/deploy/SKILL.md index b1c8b1e..31f1826 100644 --- a/.gemini/skills/deploy/SKILL.md +++ b/.gemini/skills/deploy/SKILL.md @@ -9,25 +9,24 @@ description: Code Cannon: Bump the project version, create a GitHub Release, and ## What `/deploy` does -`/deploy` is the final step in the workflow. It combines version bumping and release creation into a single command: check state, optionally bump the version, then create a GitHub Release (and in multi-branch mode, promote to production). +`/deploy` is the final step in the workflow. It combines version bumping and release creation into a single command: check state, optionally bump the version, then create a GitHub Release (and in multi-branch mode, promote the deploy branch to production first). + +The branching mode changes the shape of the release: in **trunk mode** (`BRANCH_PROD` only) `/deploy` tags and releases the current branch directly; in **multi-branch mode** (`BRANCH_DEV` set, optionally with `BRANCH_TEST`) it first opens and merges a release PR from the deploy branch into production, and that merge is what closes the linked issues. --- -## Step 1 — Verify branch +## Step 1 — Verify branch and sync -Run: -```bash -git branch --show-current -``` +Run `git branch --show-current`. The **deploy branch** for this project is: -Required branch: `dev` (two-branch mode). +`dev` (two-branch mode). -If not on the required branch, abort and say: "Switch to `<required-branch>` before running `/deploy`." +If not on the deploy branch, abort: "Switch to `<deploy-branch>` before running `/deploy`." -Sync to the remote before proceeding. The script below guards against uncommitted local changes, then runs `git checkout`, `git fetch`, and `git reset --hard origin/<base>` as one atomic operation. The deploy branch is never edited locally under the CodeCannon workflow (only fast-forwarded from CodeCannon's own merges), so the hard reset is the correct sync; the dirty-tree guard catches accidental local edits before they get silently discarded. +Then sync it to the remote. The script guards against uncommitted local changes, then runs `git checkout`, `git fetch`, and `git reset --hard origin/<deploy-branch>` as one atomic operation. The deploy branch is never edited locally under the CodeCannon workflow (only fast-forwarded from merges), so the hard reset is the correct sync; the dirty-tree guard catches accidental local edits before they are silently discarded. ```bash -python3 CodeCannon/skills/github-agile/scripts/sync-base-branch.py dev +python3 CodeCannon/skills/github-agile/scripts/sync-base-branch.py <deploy-branch> ``` If the script exits non-zero, stop and resolve the issue it reports before continuing. @@ -36,87 +35,35 @@ If the script exits non-zero, stop and resolve the issue it reports before conti ## Step 2 — Check current state -### Find the latest version tag - -```bash -git describe --tags --abbrev=0 -``` +Find the latest version tag (`git describe --tags --abbrev=0`; if none, note this is the first release) and read the current version with `cat VERSION`. -If no tag exists, note this is the first release. - -### Read current version +Show the merge commits (and their PRs) since the last tag. The range depends on the mode: ```bash -cat VERSION +git log main..<deploy-branch> --merges --pretty=format:"%s" ``` -### Show commits since last tag +Merge-commit subjects have the form `Merge pull request #N from branch/name` — parse the PR numbers, then retrieve each body with `gh pr view <N> --json number,title,body`. -If a previous tag exists, show what's on the branch since that tag: +From those PR bodies, compile the release's issue links: -```bash -git log main..dev --merges --pretty=format:"%s" -``` - -Parse PR numbers from merge commit subjects (format: `Merge pull request #N from branch/name`). +Keep closing keywords and context references **separate — do not merge them into one set**: -For each PR number found, retrieve the PR body: -```bash -gh pr view <N> --json number,title,body -``` +- **Close set** — the union of every `Closes #N` line across all constituent PR bodies. These auto-close when the release PR merges into `main`. Record, per constituent PR, the exact `Closes #N` lines so they can be reproduced verbatim in the release PR body. +- **Reference set** — issues mentioned only via `Related to #N` or the legacy `Issue #N` form. These are context links and will **not** close. Legacy `Issue #N` carries no recoverable close-intent, so it stays in the reference set rather than being guessed into the close set; the human gate surfaces it so you can manually close any straggler that should have closed. +- **PRs included** (number + title). -Extract closing keywords **separately** from context references — do **not** merge them into a single set: +Also check for open unmerged PRs (`gh pr list --state open --json number,title,headRefName`). -- **Close set** — the union of every `Closes #N` line across all constituent PR bodies. These issues will auto-close when the release PR merges into `main`. Record, per constituent PR, the exact `Closes #N` lines it contained so they can be reproduced verbatim in the release PR body. -- **Reference set** — issues mentioned only via `Related to #N`, or via the legacy `Issue #N` form. These are context links and will **not** close. Legacy `Issue #N` carries no recoverable close-intent, so it stays in the reference set rather than being guessed into the close set; the HUMAN GATE surfaces it so you can manually close any straggler that should have been a `Closes`. - -Compile: -- List of PRs included (number + title) -- Close set and reference set, kept distinct - -### Check for open unmerged PRs - -```bash -gh pr list --state open --json number,title,headRefName -``` - -### Present the summary - -Tell the user: - -``` -Current version: X.Y.Z -Latest tag: vX.Y.Z - -Commits/PRs since last tag: - #17 — Add /docs directory - #18 — Fix checkout runtime error - -Open PRs not yet merged: - #19 — Add dark mode (feature/dark-mode) - -Would you like to bump the version before deploying? - - **patch** → X.Y.C - - **minor** → X.B.0 - - **major** → A.0.0 - - **specific** → enter a version number - - **skip** → proceed to release with the latest existing tag -``` - -Wait for their response. +Present a summary — current version, latest tag, the PRs/issues since that tag, any open PRs — and ask whether to bump the version before deploying (patch → X.Y.C, minor → X.B.0, major → A.0.0, a specific version, or skip to release the latest existing tag). Wait for their response. --- ## Step 3 — Version bump (if requested) -If the user chose to skip, find the latest version tag in the branch history: -```bash -git describe --tags --abbrev=0 -``` - -If no tag is found at all (first release), warn: "No version tag found. You must bump the version before deploying." Return to the version bump prompt. Otherwise, use the tag found as the release version. +If the user chose **skip**, use the latest existing tag (`git describe --tags --abbrev=0`) as the release version. If none exists (first release), warn "No version tag found. You must bump the version before deploying." and return to the bump prompt. -If the user chose a bump level, map their response to a bump command and run `bump-and-tag.py`, which performs the bump, verifies the resulting tag (creating an annotated fallback if `tag.forceSignAnnotated` silently rejected a lightweight tag), and pushes both the commit and the tag. The resolved version is printed on stdout — capture it for the release step. +If the user chose a bump level, map it to a bump command and run `bump-and-tag.py`, which performs the bump, verifies the resulting tag (creating an annotated fallback if `tag.forceSignAnnotated` silently rejected a lightweight tag), and pushes both the commit and the tag. The resolved version is printed on stdout — capture it as `<new-version>`. | User says | `--bump-cmd` | |---|---| @@ -131,59 +78,37 @@ python3 CodeCannon/skills/github-agile/scripts/bump-and-tag.py \ --version-read-cmd "cat VERSION" ``` -If the script exits non-zero, stop and resolve the issue it reports before continuing. On success, the version printed on stdout is the new version — use it as `<new-version>` in subsequent steps. +If the script exits non-zero, stop and resolve the issue it reports before continuing. --- ## Step 4 — Compute release contents -Determine the version tag (either from the bump just performed, or from the existing HEAD tag if the user skipped bumping). +Determine the release version tag (from the bump just performed, or the existing HEAD tag if the user skipped). Find the previous tag for the changelog range: `git describe --abbrev=0 <version-tag>^`. -Find the previous tag to determine the range: -```bash -git describe --abbrev=0 <version-tag>^ -``` - -Use the PR list, close set, and reference set already computed in Step 2. If the version bump added new commits, re-fetch if needed. +Reuse the PR list, close set, and reference set already computed in Step 2. If the version bump added new commits, re-fetch as needed. --- ## Step 5 — HUMAN GATE -Show the user the release summary. Example format: - -``` -Ready to release vX.Y.Z to production. +Show the release summary — the target version, the PRs included, and the issue links: -PRs included: - #17 — Add /docs directory - #18 — Fix checkout runtime error +- Issues that will **close** on merge (the close set, reproduced verbatim from constituent PRs). +- Issues **referenced but not closing** (the reference set — confirm none of these should actually close). -Issues that will close on merge (Closes #N, reproduced verbatim from constituent PRs): - #14 — Add /docs directory - #15 — Fix checkout runtime error - -Issues referenced but NOT closing (Related to #N / legacy Issue #N — confirm none of these should actually close): - #20 — Tighten error copy on the upload form - -Have you tested all of the above on preview? Type 'release' to confirm. -``` +Confirm the deploy branch has been tested: +"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. +Wait for "release" or an explicit confirmation. Any other response → stop and ask what they'd like to change. --- -## Step 6 — Create PR: `dev` → `main` +## Step 6 — Promote: `<deploy-branch>` → `main` -First, create a temp directory for this invocation: +Create a temp directory for this invocation (`python3 CodeCannon/skills/github-agile/scripts/make-workdir.py`) and note the returned path — use it for all temp files here. -```bash -python3 CodeCannon/skills/github-agile/scripts/make-workdir.py -``` - -Note the returned path (e.g. `/tmp/CodeCannon/a8f3b2`). Use this path for all temp files in this invocation. - -Then use your file-writing tool (not Bash) to create `<tmpdir>/release_pr_body.md`: +Use your file-writing tool (not Bash) to create `<tmpdir>/release_pr_body.md`: ```markdown Release vX.Y.Z @@ -198,53 +123,31 @@ Closes #15 Related to #20 ``` -Reproduce **every** `Closes #N` line from the close set computed in Step 2 — verbatim, one per line, omitting none. Add a `Related to #N` line for each issue in the reference set so the links appear on the release PR without triggering an auto-close. If the reference set is empty, omit the `Related to` lines entirely. +Reproduce **every** `Closes #N` line from the close set — verbatim, one per line, omitting none. Add a `Related to #N` line for each issue in the reference set so the links appear without triggering an auto-close; if the reference set is empty, omit the `Related to` lines entirely. -Then create the PR (do NOT use `--body`, `--body-file -`, or heredocs): +Create the PR (do NOT use `--body`, `--body-file -`, or heredocs), with `--head` set to the deploy branch: ```bash -gh pr create --base main --head dev \ +gh pr create --base main --head <deploy-branch> \ --title "Release vX.Y.Z" \ --body-file <tmpdir>/release_pr_body.md ``` -Note the PR number from the output. - -The `Closes #N` lines will auto-close the linked issues because this PR merges into `main` (the default branch). - -> **Critical:** Use the unqualified `#N` form only. Never write `Closes owner/repo#N`, even for same-repo refs — GitHub's closing-keyword parser only populates `closingIssuesReferences` for the unqualified form, and the qualified form silently breaks auto-close. - ---- - -## Step 7 — Merge +> **Critical:** Use the unqualified `#N` form only. Never write `Closes owner/repo#N`, even for same-repo refs — GitHub's closing-keyword parser only populates `closingIssuesReferences` for the unqualified form, and the qualified form silently breaks auto-close. The `Closes #N` lines auto-close the linked issues because this PR merges into `main` (the default branch). -Do NOT use `make merge` — it refuses PRs targeting `main`. Use `gh pr merge` directly: - -```bash -gh pr merge <pr-number> --merge -``` +Then merge. Do NOT use `make merge` — it refuses PRs targeting `main`. Use `gh pr merge <pr-number> --merge` directly. --- -## Step 8 — Create GitHub Release +## Step 7 — Create the GitHub Release -**Publish confirmation — required before writing the release notes or creating the Release.** The GitHub Release is a public-surface action. The single word from Step 5 authorized the promotion/merge (already done); the public publish gets its own explicit confirmation. Tell the user (substitute the actual tag, e.g. `v0.13.0`): +**Publish confirmation — required before writing the release notes or creating the Release.** The GitHub Release is a public-surface action; the confirmation from Step 5 authorized the promotion, but the public publish gets its own explicit confirmation. Tell the user (substitute the actual tag, e.g. `v0.13.0`): > Publishing GitHub Release `<version-tag>` — the final public step. Confirm by pasting: `publish <version-tag>` Wait for the user to paste `publish <version-tag>` (or an explicit version-named variant such as `ship <version-tag>`). Any other response → stop and ask what they'd like to change. The version-named phrase is deliberate: Claude Code's auto-mode safety classifier requires authorization that names the release before `gh release create` runs, so the generic Step 5 confirmation is not relied on for the public publish. If a harness still blocks the call after this confirmation (e.g. an older client), the user can re-confirm with `publish <version-tag> release` to unblock. ---- - -The version tag (from Step 3) and the PR/issue list (from Step 4) are already known. Find the previous tag to build the changelog link: - -```bash -git describe --abbrev=0 <version-tag>^ -``` - -If no previous tag exists, omit the "Full changelog" line. - -Use your file-writing tool (not Bash) to create `<tmpdir>/release_notes.md` (same temp directory from Step 6): +The version tag and PR/issue list are already known; the previous tag comes from Step 4 (if there is no previous tag, omit the "Full changelog" line). Create a temp directory if you haven't already (`python3 CodeCannon/skills/github-agile/scripts/make-workdir.py`), then use your file-writing tool (not Bash) to create `<tmpdir>/release_notes.md`: ```markdown ## Changes @@ -255,7 +158,7 @@ Use your file-writing tool (not Bash) to create `<tmpdir>/release_notes.md` (sam **Full changelog:** https://github.com/<owner>/<repo>/compare/<previous-tag>...<version-tag> ``` -Then create the release (do NOT use `--notes`, `--notes-file -`, or heredocs): +Format each PR line as `- #<linked-issue> — <PR title> (PR #<N>)`; if a PR had no linked issue, use just the PR title. Then create the release (do NOT use `--notes`, `--notes-file -`, or heredocs): ```bash gh release create <version-tag> \ @@ -263,15 +166,11 @@ gh release create <version-tag> \ --notes-file <tmpdir>/release_notes.md ``` -Format each PR line as `- #<linked-issue> — <PR title> (PR #<N>)`. If a PR had no linked issue, omit the `#<issue>` prefix and use just the PR title. - -After the command runs, note the release URL from the output. +Note the release URL from the output. --- -## Step 9 — Report - -Tell the user: +## Step 8 — Report -> "Released vX.Y.Z. Issues #N, #M closed automatically. GitHub Release vX.Y.Z created at `<url>`. Run `make deploy-prod` to ship to production." -<!-- generated by CodeCannon/sync.py | skill: deploy | adapter: gemini | hash: 4c21b5df | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +Tell the user: "Released vX.Y.Z. Linked issues are closed. GitHub Release vX.Y.Z created at `<url>`. Run `make deploy-prod` to ship to production." +<!-- generated by CodeCannon/sync.py | skill: deploy | adapter: gemini | hash: 904fbcdf | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.gemini/skills/start/SKILL.md b/.gemini/skills/start/SKILL.md index 1e4caea..f248cef 100644 --- a/.gemini/skills/start/SKILL.md +++ b/.gemini/skills/start/SKILL.md @@ -27,12 +27,10 @@ Otherwise → go to **Case A: New work**. > **Execution order:** Resolve labels and milestones **now**, before entering Case A Step 1. If milestone auto-detection requires a user prompt (2+ open milestones), that prompt happens here — not later during issue creation. By the time you reach Step 2's human gate, all metadata must already be resolved so that Step 3 can proceed without re-prompting. -The argument string may contain optional inline flags after the description. Parse as follows: +The description may be followed by optional flags — `--label`/`-l` and `--milestone`/`-m`, in any order. Separate the description from the flags yourself; the flags carry these meanings: -1. **Identify flags** — scan for the first token that starts with `--label`, `-l`, `--milestone`, or `-m`. Everything before it is the **description**. Everything from the first flag onward is **flags**. -2. **`--label <value>` / `-l <value>`** — comma-separated label string (e.g. `bug` or `enhancement,ux`). If provided, it **bypasses label auto-selection entirely** for this invocation — use the value verbatim. Labels containing spaces must be quoted (e.g. `--label "good first issue"`). -3. **`--milestone <value>` / `-m <value>`** — milestone name or number (e.g. `Sprint 4` or `12`). Pass the value as-is; GitHub accepts both names and numbers. -4. **Flags may appear in any order** after the description. +- **`--label <value>` / `-l <value>`** — a comma-separated label string used **verbatim**, bypassing label auto-selection entirely for this invocation. Quote values containing spaces (e.g. `--label "good first issue"`). +- **`--milestone <value>` / `-m <value>`** — a milestone name or number (GitHub accepts both names and numbers). **Label resolution (three-tier, Case A only):** @@ -57,14 +55,6 @@ After parsing flags, determine the active milestone in this order: - **1 result** → use its title silently. Inform the user inline: `(milestone: <title>)`. - **2+ results** → show the numbered list, ask once: **"Multiple open milestones — which should this issue go under? (enter a number or title, or 'none')"**. Accept milestone number, title, or "none"/"skip". Wait for response before continuing. -**Examples:** - -| `$ARGUMENTS` | Description | Labels | Milestone | -|---|---|---|---| -| `Add dark mode toggle to settings page` | `Add dark mode toggle to settings page` | auto-selected from pool | auto-detected | -| `Add dark mode --label enhancement` | `Add dark mode` | `enhancement` (verbatim) | auto-detected | -| `Add dark mode --label enhancement,ux --milestone "Sprint 4"` | `Add dark mode` | `enhancement,ux` (verbatim) | `Sprint 4` | - > Replace vs append: flags **replace** auto-selection entirely, they do not append. This avoids silent label duplication and milestone conflicts. --- @@ -102,7 +92,7 @@ If on any other branch → proceed to Case A or Case B as determined by the `$AR ### Step 1 — Investigate -Read the relevant code. Propose a concrete implementation approach. Be specific about which files change and how. +Read the relevant code using your harness's native file-reading and search tools (read, grep/glob, and the like) rather than shell pipelines — hand-rolled `find … | xargs`, `grep ; awk`, or redirection shapes trigger permission prompts that cannot be permanently allowed. Then propose a concrete implementation approach, specific about which files change and how. ### Step 2 — HUMAN GATE @@ -222,7 +212,14 @@ Show the user: `On branch feature/<name>` ### Step 5 — Write the code -Now write the code. Do NOT commit anything. +Write the code using your harness's native editing tools. Do NOT commit anything. +To exercise your work as you go, run the project's configured test command rather than hand-rolling a test invocation — a hand-built `python3 -m unittest … > /tmp/…` or similar redirection shape triggers permission prompts that a configured command avoids: + +```bash +make test +``` + +If it fails because the target does not exist, tell the user rather than improvising a replacement. When done, say: **"When you've verified locally, reply `yes` to submit, or say what to change."** @@ -317,7 +314,14 @@ git branch --show-current ### Step 5 — Write the code -Continue from where work left off. Do NOT commit. +Continue from where work left off, using your harness's native editing tools. Do NOT commit. +To exercise your work as you go, run the project's configured test command rather than hand-rolling a test invocation — a hand-built `python3 -m unittest … > /tmp/…` or similar redirection shape triggers permission prompts that a configured command avoids: + +```bash +make test +``` + +If it fails because the target does not exist, tell the user rather than improvising a replacement. When done, say: **"When you've verified locally, reply `yes` to submit, or say what to change."** @@ -336,4 +340,4 @@ When done, say: **"When you've verified locally, reply `yes` to submit, or say w - 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 <number> --remove-assignee @me`. - Apply resolved labels and milestone to every new issue. Label resolution order: per-invocation flag → pool selection from `bug, documentation, enhancement, chore` → omit `--label` entirely. Never apply a label outside `bug, documentation, enhancement, chore`. - Milestone resolution order: per-invocation flag → auto-detected from GitHub open milestones. Never prompt for a milestone more than once per invocation. -<!-- generated by CodeCannon/sync.py | skill: start | adapter: gemini | hash: 0af62e25 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: start | adapter: gemini | hash: 21fde40c | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.gemini/skills/status/SKILL.md b/.gemini/skills/status/SKILL.md index ae4683f..0863106 100644 --- a/.gemini/skills/status/SKILL.md +++ b/.gemini/skills/status/SKILL.md @@ -7,45 +7,39 @@ description: Code Cannon: Summarize in-progress and recently completed work from --- -## Step 1 — Parse arguments +## What `/status` does -First, check whether `$ARGUMENTS` contains `--milestone`, `--sprint`, or `--team`. +`/status` prints a read-only, standup-ready snapshot of in-progress and recently completed work, then a single "what's next" suggestion. It never writes to GitHub or the working tree — it only reads and reports. -**Milestone mode:** If `--milestone` or `--sprint` is present, extract everything after the flag as the milestone name (trim leading/trailing whitespace; preserve internal spaces). Ignore any other arguments. Enter milestone mode (Steps M1–M3 below) and skip Steps 2–6. +Because it is read-only, the *shape* of its output does not matter: a differently-formatted-but-accurate summary is a fine result. Derive a clear, scannable layout yourself. What this skill pins down is the data to fetch, how to classify it, and the one piece of real opinion — the "what's next" ordering. -Examples: -- `--milestone Sprint 4` → milestone name = `Sprint 4` -- `--sprint Sprint 4` → milestone name = `Sprint 4` -- `--milestone Q2 Release` → milestone name = `Q2 Release` -- `--milestone 12` → milestone name = `12` - -**Team mode:** If `--team` is present, enter team mode (Steps T1–T3 below) and skip Steps 2–6. `--team` is mutually exclusive with `--milestone`/`--sprint` and username arguments. If both are present, report the conflict and stop. +--- -**Personal mode** (no `--milestone` / `--sprint` / `--team` flag): determine: +## Step 1 — Determine mode -- **subject**: default `@me`. If the argument starts with `@` or is a plain word that is not a number, treat it as a GitHub username. Strip the leading `@` for `gh` commands that do not accept it (e.g. `gh pr list --author alice`); keep it for display. -- **lookback**: default `7`. If the argument is a number (digits only), use it as the lookback window in days. +Three mutually exclusive modes, selected from `$ARGUMENTS`: -No argument → subject = `@me`, lookback = `7`. +- **Milestone mode** — `--milestone` or `--sprint` is present. Everything after the flag is the milestone name (a name or a number; trim outer whitespace, preserve internal spaces). Ignore other arguments. Run Steps M1–M2. +- **Team mode** — `--team` is present. Run Steps T1–T2. `--team` is mutually exclusive with `--milestone`/`--sprint` and with a username; if combined, report the conflict and stop. +- **Personal mode** — no mode flag. **Subject** defaults to `@me`; a `@name` or a non-numeric word is a username (strip the leading `@` for `gh` flags that reject it, keep it for display). **Lookback** defaults to `7`; a bare number is the lookback in days. --- -## Step 2 — Fetch GitHub data (run all in parallel) +## Step 2 — Fetch GitHub data (personal mode) -Run these commands concurrently: +Run these concurrently. If any `gh` command exits non-zero (including auth errors), report the message and stop — do not retry. -**Open PRs authored by subject:** +**Open PRs authored by subject** — request enough fields to derive health (draft, CI, review decision, merge conflict) and staleness: ```bash gh pr list --author <subject> --state open \ --json number,title,url,labels,milestone,baseRefName,body,reviewDecision,statusCheckRollup,updatedAt,mergeable,isDraft ``` -**Recently merged PRs (last `<lookback>` days):** +**Recently merged PRs**, filtered to those merged within `<lookback>` days: ```bash gh pr list --author <subject> --state merged --limit 20 \ --json number,title,url,mergedAt,labels,baseRefName ``` -Filter the results to keep only entries where `mergedAt` is within the last `<lookback>` days. **Open issues assigned to subject:** ```bash @@ -53,317 +47,103 @@ gh issue list --assignee <subject> --state open \ --json number,title,url,labels,milestone,updatedAt ``` -**PRs requesting your review** (only when subject is `@me`): +**PRs requesting your review** — only when subject is `@me`; skip for other users: ```bash gh pr list --search "review-requested:@me" --state open \ --json number,title,url,author,updatedAt ``` -Skip this query when viewing another user's status. -If any `gh` command exits with a non-zero status (including auth errors), report the error message and stop. Do not retry. - ---- - -## Step 3 — Fetch local git context - -Check if the current directory is inside a git repository: -```bash -git rev-parse --is-inside-work-tree -``` - -If yes, run: +Also fetch local git context (skip and note if not in a git repo — `git rev-parse --is-inside-work-tree`): ```bash git log --oneline --since="<lookback> days ago" ``` -If not inside a git repo, skip this step and note it was skipped in the output. - --- -## Step 4 — Classify items - -Using the data from Steps 2 and 3, classify each item: - -- **In progress** — open PRs. For each, attempt to identify a linked issue number from the PR body (look for `#N`, `closes #N`, `fixes #N`, `issue #N`). If found, cross-reference with open issues. -- **Done** — merged PRs within the lookback window. -- **Up next** — open issues that are NOT associated with any open PR (i.e. no open PR body references their issue number). -- **Needs your review** — PRs from the review-requested query (only when subject is `@me`). - -An open issue that IS linked from an open PR body appears under "In progress" alongside that PR, not under "Up next". +## Step 3 — Classify and report (personal mode) -### 4a — Derive health badges +Sort items into these buckets: -For each open PR, derive the following badges: +- **In progress** — open PRs. Identify a linked issue from the PR body (`#N`, `closes #N`, `fixes #N`, `issue #N`) and cross-reference open issues. +- **Done** — PRs merged within the lookback window. +- **Up next** — open issues whose number is **not** referenced by any open PR body. (An issue linked from an open PR belongs under *In progress* with that PR, not here.) +- **Needs your review** — the review-requested query (only when subject is `@me`). -**Draft status:** -- If `isDraft` is `true` → `[draft]` +For each open PR, derive health from the JSON — draft state, CI status from `statusCheckRollup`, review state from `reviewDecision`, merge conflict from `mergeable`. Present each as a compact badge; omit a badge when it does not apply or is not configured. -**CI check status** (from `statusCheckRollup`): -- All checks have `status: COMPLETED` and `conclusion: SUCCESS` → `✅ checks passing` -- Any check has `conclusion: FAILURE` → `❌ checks failing` -- Checks are still running or have other states → `⏳ checks pending` -- No checks configured → omit badge +**Staleness:** flag any open PR or issue not updated within `14` days (a threshold of `0` disables staleness entirely). Note the last-updated date and age. This is a real config-driven rule — honor the threshold exactly. -**Review decision** (from `reviewDecision`): -- `APPROVED` → `✅ approved` -- `CHANGES_REQUESTED` → `🔄 changes requested` -- `REVIEW_REQUIRED` or empty → `⏳ awaiting review` +Report the buckets as a scannable summary: a heading naming the subject and lookback, a one-line count roll-up, then a section per non-empty bucket, then the local commits (or a note that git was skipped). Show labels/milestone only when present; dates as `YYYY-MM-DD`. If every GitHub bucket is empty, say so plainly for the subject and window. -**Merge conflict** (from `mergeable`): -- `CONFLICTING` → `⚠️ conflicts` -- `MERGEABLE` or `UNKNOWN` → omit badge - -### 4b — Flag stale items - -For each open PR and open issue, check `updatedAt`. If the item has not been updated within `14` days (default: 14; disabled when set to 0), flag it as stale. Record the last-updated date and the number of days since the last update. - -A stale item gets an inline `⚠️ stale (<N>d)` badge appended after any other badges. +**Do not post, comment, write files, or take any action. Output only.** --- -## Step 5 — Output the summary - -Print a formatted summary. Use this structure: - -``` -## Status for <subject> — last <lookback> days - -<N> in progress · <N> done · <N> up next[ · <N> need your review] - -### In progress -- #<number> <title> [<labels>] [<milestone>] [draft] - PR: <url> · <check badge> · <review badge>[ · <conflict badge>][ · <stale badge>] - Linked issue: #<number> (if found) +## Step 4 — What's next (personal mode) -### Done -- #<number> <title> [<labels>] — merged <date> - PR: <url> +After the summary, append **one** actionable suggestion. Gather the extra local state you need (current branch, `git status --porcelain`, latest tag via `git describe --tags --abbrev=0`, unreleased commit count via `git rev-list <tag>..HEAD --count`, and the current branch's PR review/check state via `gh pr view` — treat a non-zero exit as "no PR for this branch"). Skip git lookups when not in a repo. -### Needs your review -- #<number> <title> (by @<author>) - PR: <url> +Evaluate these conditions **in order** and use the **first** match. This ordering is the workflow's opinion about what the operator should do next — it is load-bearing, not formatting: -### Up next -- #<number> <title> [<labels>] [<milestone>][ · <stale badge>] - Issue: <url> +| Priority | Condition | Suggestion | +|----------|-----------|------------| +| 1 | On a `feature/*` branch with uncommitted changes | You have uncommitted changes on `<branch>`. When ready, run `/submit-for-review`. | +| 2 | On a `feature/*` branch, open PR is `APPROVED` and all checks `COMPLETED` | PR #<number> is approved and checks pass. Consider running `/deploy`. | +| 3 | On a `feature/*` branch with an open PR in any other state | PR #<number> (<title>) is open and awaiting review. | +| 3.5 | Subject is `@me` and PRs request your review | Append to the current suggestion (or stand alone if nothing higher matched): You also have <N> PR(s) awaiting your review. | +| 4 | On a `feature/*` branch, no open PR, clean tree | No open PR for `<branch>`. Run `/submit-for-review` to open one. | +| 5 | On the integration branch with unreleased commits since the last tag | <N> commit(s) on `<branch>` since `<tag>`. Run `/deploy` when ready to release. | +| 6 | No open PRs and no open issues assigned to subject | Nothing in progress. Run `/start` to begin new work. | +| 7 | Open issues exist in "Up next" | Next up is #<number> (<title>). Run `/start <number>` to pick it up. | -### ⚠️ Stale -- #<number> <title> — last updated <date> (<N> days ago) +If none match, omit the "what's next" section. Omit it entirely in milestone mode. --- -Local commits (current branch): -<git log output, or "skipped — not in a git repo"> -``` -Rules: -- **Summary counts line**: show immediately after the heading. Omit zero-count segments (e.g., if nothing is done, skip that segment). "need your review" only appears when subject is `@me` and the count is > 0. -- **Health badges**: show on the second line of each "In progress" item, after the PR URL, separated by ` · `. Omit individual badges that don't apply (e.g., no conflict badge if mergeable). -- **Draft badge**: show `[draft]` inline in the first line of draft PRs, before any other badges. -- **Stale section**: a dedicated section at the bottom (before "Local commits") listing all stale items from any section, with their last-updated date and age. This gives a consolidated view. Individual items also get the inline `⚠️ stale (<N>d)` badge in their own sections. -- **"Needs your review" section**: only shown when subject is `@me` and there are PRs requesting review. Placed between "Done" and "Up next". -- Omit any section that has no items — do not show an empty heading. -- Show labels only if present; show milestone only if present. -- Dates use `YYYY-MM-DD` format. -- If all GitHub sections are empty, print: `Nothing found for <subject> in the last <lookback> days.` - -Do not post, comment, write files, or take any action. Output only. - ---- - -## Step 6 — What's next - -After the status summary, append a single actionable suggestion based on local git state and the GitHub data already fetched. - -### 6a — Gather additional local state - -Run these commands (skip if not in a git repo): - -```bash -git branch --show-current -``` - -```bash -git status --porcelain -``` - -```bash -git describe --tags --abbrev=0 -``` - -```bash -git rev-list <latest-tag>..HEAD --count -``` - -From the GitHub data fetched in Step 2, also check for the current branch's PR approval status: - -```bash -gh pr view --json number,title,url,reviewDecision,statusCheckRollup \ - --jq '{number,title,url,reviewDecision,checks: [.statusCheckRollup[]? | .status]}' -``` - -If `gh pr view` exits non-zero (no PR for current branch), note that there is no open PR. - -### 6b — Determine suggestion - -Evaluate the following conditions **in order**. Use the **first** match: - -| Priority | Condition | Output | -|----------|-----------|--------| -| 1 | On a `feature/*` branch with uncommitted changes (`git status --porcelain` is non-empty) | `What's next: You have uncommitted changes on \`<branch>\`. When ready, run \`/submit-for-review\`.` | -| 2 | On a `feature/*` branch with an open PR that has `reviewDecision: APPROVED` and all status checks are `COMPLETED` | `What's next: PR #<number> is approved and checks pass. Consider running \`/deploy\`.` | -| 3 | On a `feature/*` branch with an open PR (any other review/check state) | `What's next: PR #<number> (<title>) is open and awaiting review.` | -| 3.5 | Subject is `@me` and there are PRs requesting your review (from Step 2 query) | Append to the current suggestion (or show standalone if no higher priority matched): `You also have <N> PR(s) awaiting your review.` | -| 4 | On a `feature/*` branch with no open PR and clean working tree | `What's next: No open PR for \`<branch>\`. Run \`/submit-for-review\` to open one.` | -| 5 | On the integration branch (`dev`, `develop`, or `main` when no integration branch exists) with unreleased commits (rev-list count > 0 since last tag) | `What's next: <N> commit(s) on \`<branch>\` since \`<tag>\`. Run \`/deploy\` when ready to release.` | -| 6 | No open PRs, no open issues assigned to subject | `What's next: Nothing in progress. Run \`/start\` to begin new work.` | -| 7 | Open issues exist in "Up next" | `What's next: Next up is #<number> (<title>). Run \`/start <number>\` to pick it up.` | - -If none of the above match, omit the "What's next" section entirely. - -### 6c — Format - -Print the suggestion after a horizontal rule, below the local commits section: - -``` ---- -🧭 <suggestion text> -``` - -This section is omitted in milestone mode. - ---- - -## Milestone mode (Steps M1–M3) - -Only entered when `--milestone` or `--sprint` is detected in Step 1. - -### Step M1 — Fetch milestone issues +## Milestone mode (Steps M1–M2) +### M1 — Fetch ```bash gh issue list --milestone "<name>" --state all --limit 200 \ --json number,title,state,labels,assignees,url -``` - -If this command fails for any reason (milestone not found, auth error, etc.), report the error and stop. - -### Step M2 — Classify issues - -Fetch all open PRs to detect which issues are in progress (with health fields): - -```bash gh pr list --state open \ --json number,title,body,baseRefName,reviewDecision,statusCheckRollup,mergeable,isDraft ``` +If the issue query fails (milestone not found, auth error), report and stop. -Group issues into three buckets: - -- **Done** — `state: closed` -- **In progress** — `state: open` AND the issue number appears in any open PR body (look for `#<number>`, `closes #<number>`, `fixes #<number>`, `related to #<number>`, `issue #<number>`) -- **Not started** — `state: open` AND no open PR body references the issue number - -For in-progress issues, derive health badges from the linked PR using the same rules as Step 4a (check status, review decision, draft, conflict). - -### Step M3 — Output the summary - -``` -## Sprint: <name> - -<Y> of <total> issues closed · <Z> in progress · <W> not started - -### In progress (<Z>) -- #<number> <title> [@<assignee>] [<milestone>][ [draft]] - <url> · <check badge> · <review badge>[ · <conflict badge>] - -### Not started (<W>) -- #<number> <title> [@<assignee>] +### M2 — Classify and report -### Done (<Y>) -- #<number> <title> -``` - -Rules: -- Show "In progress" first, then "Not started", then "Done" -- Show assignee only if present; omit if unassigned -- Show URLs only for in-progress items; omit URLs for closed issues -- Show health badges on in-progress items (same derivation as Step 4a) -- If a section has no items, omit it entirely +Group the milestone's issues into three buckets: +- **Done** — `state: closed`. +- **In progress** — open, and the issue number is referenced by some open PR body (`#N`, `closes #N`, `fixes #N`, `related to #N`, `issue #N`). This "referenced by an open PR" definition is the real rule — apply it exactly. +- **Not started** — open, and no open PR references it. -Do not post, comment, write files, or take any action. Output only. +Derive health badges for in-progress issues from their linked PR (same fields as personal mode). Report as a scannable summary titled with the milestone name and a closed/in-progress/not-started roll-up. Show assignees and URLs where they add value; omit empty buckets. **Do not post, comment, write files, or take any action. Output only.** --- -## Team mode (Steps T1–T3) - -Only entered when `--team` is detected in Step 1. - -### Step T1 — Fetch all open work (run both in parallel) +## Team mode (Steps T1–T2) +### T1 — Fetch (run both concurrently) ```bash gh pr list --state open --limit 100 \ --json number,title,url,author,labels,milestone,baseRefName,body,reviewDecision,statusCheckRollup,updatedAt,mergeable,isDraft -``` - -```bash gh issue list --state open --limit 200 \ --json number,title,url,assignees,labels,milestone,updatedAt ``` +If either fails, report and stop. -If either command fails, report the error and stop. - -### Step T2 — Group and classify - -Group items by person: -- PRs are grouped by `author.login` -- Issues are grouped by assignee (first assignee if multiple). Issues with no assignee go into an "Unassigned" group. - -Within each person's group, classify items the same way as personal mode (Step 4): -- **In progress** — open PRs (and linked issues) -- **Up next** — open issues not linked from any open PR - -Derive health badges (Step 4a) and flag stale items (Step 4b) for all items. - -### Step T3 — Output the team summary - -``` -## Team status - -<N> open PRs · <N> open issues · <N> people - -### @<person> (<N> in progress, <N> up next) -- #<number> <title> — PR <check badge> · <review badge>[ · <conflict badge>][ · <stale badge>][ [draft]] - <url> -- #<number> <title> [up next][ · <stale badge>] - -### @<person> (<N> in progress, <N> up next) -... - -### Unassigned (<N>) -- #<number> <title> - <url> - -### ⚠️ Stale -- #<number> <title> (@<person>) — last updated <date> (<N> days ago) -``` - -Rules: -- Sort people alphabetically by username -- Within each person, show in-progress items first, then up-next items -- Show health badges on PR items (same format as personal mode) -- Show `[draft]` on draft PRs -- Tag up-next items with `[up next]` for visual distinction -- "Unassigned" section appears at the bottom, only if there are unassigned issues -- "Stale" section consolidates all stale items across all people -- Omit any section or group with no items -- No "What's next" section in team mode +### T2 — Group and report -Do not post, comment, write files, or take any action. Output only. +Group by person: PRs by `author.login`; issues by first assignee (unassigned issues into an "Unassigned" group). Within each person, classify as personal mode does — **in progress** (open PRs and their linked issues) and **up next** (open issues not referenced by any open PR) — and derive health badges plus staleness. Report per-person sections with in-progress items first, an "Unassigned" section if any, and a consolidated stale section. **Do not post, comment, write files, or take any action. Output only.** --- ## Hard rules -- Never write to GitHub (no comments, labels, issue updates, or PR changes). -- 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. -<!-- generated by CodeCannon/sync.py | skill: status | adapter: gemini | hash: 11176f8f | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +- Never write to GitHub (no comments, labels, issue updates, or PR changes) and never touch the working tree. Output only. +- If `gh` is unauthenticated or any fetch fails, report the error and stop immediately. Do not retry. +- Strip the leading `@` from the subject when passing to `gh` flags that reject it. +- `--team` is mutually exclusive with `--milestone`/`--sprint` and with a username. +- The `14` threshold is config-driven; `0` disables staleness. The "what's next" priority ordering is fixed — evaluate top to bottom, first match wins. +<!-- generated by CodeCannon/sync.py | skill: status | adapter: gemini | hash: 203ddda9 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/AGENTS.md b/AGENTS.md index 75905a5..1028c3e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -89,6 +89,26 @@ ROADMAP.md Future ideas and planned work .codecannon.yaml Project config (also listed under Configuration) ``` +## Skill design philosophy + +CodeCannon's skills were first written for a generation of models that needed to be told not just *what* outcome to produce but *how* to produce it — how to parse an argument string, what date format to use, which emoji maps to which CI state. Capable agents do all of that well unaided. Procedural over-specification no longer buys reliability; it burns context, crowds out the instructions that matter, and blocks a capable agent from doing something smarter than the author imagined. + +Every instruction in a skill must pass one test: + +> **Prune where model variance produces a different-but-fine result. Keep where model variance produces a wrong result.** + +- **Prune side** — report formatting, argument tokenizing, investigation method, output templates, badge-to-emoji mappings. A differently-shaped-but-correct result is harmless, so hand the agent the intent and the inputs, not the procedure. +- **Keep side** — ordering guarantees, human approval gates (and their exact wording), platform behaviour the model cannot derive, review policy, and the prompt-avoidance instructions (`--body-file` / no-heredoc / no-`$(cat)`). Variance in any of these is a defect, not a style difference. + +Two categories look like removable noise when read quickly but are load-bearing — never weaken them: + +- **Prompt-avoidance guidance.** The `--body-file` and no-heredoc blocks exist *because* embedding markdown in a shell command triggers permission prompts that cannot be permanently allowed. State the rule once here and point at it; do not restate it per skill and do not soften it. +- **Platform-behaviour notes.** Facts like "unqualified `#N` populates `closingIssuesReferences`", "`Closes` is inert on non-default-branch merges", and "`gh issue develop --base` reads from the API, not local working state" are knowledge a model has no way to derive. Keep them verbatim. + +Calibrate the prune against the **weakest** supported harness and model size, not the strongest. Skills ship to several adapters and a range of model sizes; trusting the agent as far as the best available model would under-serve everyone else. The cost of variance decides where the line sits — not the capability of the strongest model. + +`story.md` is the reference shape: overwhelmingly *what* rather than *how*, with business rules collected under a `## Hard rules` section at the end and procedure written only where sequencing genuinely matters. Converge older skills toward it. + ## Skill anatomy Each skill in `skills/` follows this structure: diff --git a/README.md b/README.md index 6c9204b..972f0b4 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,18 @@ Plus `/qa` for structured QA workflows and `/setup` for guided onboarding. **Configure, don't fork.** Skills use `{{PLACEHOLDER}}` tokens. Your `.codecannon.yaml` fills them in. When upstream improves, pull the submodule and re-sync. +## What makes a good skill + +Anyone can write a skill. What separates a good one from a bad one is not how thoroughly it dictates procedure — it is **token economy**, **respect for the developer's attention**, and knowing **which decisions belong to the workflow versus which belong to the agent**. + +Early skills, written for weaker models, spelled out not just *what* outcome to produce but *how* to produce it: how to parse an argument string, what date format to use, which emoji maps to which CI state. Capable agents do all of that unaided. Over-specification looks rigorous and is actually fragile — it burns context on instructions the model does not need, it breaks whenever the underlying tool changes, and it stops a capable agent from doing something smarter than the author imagined. + +Code Cannon holds every instruction to one test: + +> **Prune where model variance produces a different-but-fine result. Keep where model variance produces a wrong result.** + +Report formatting, argument parsing, and investigation method fall on the *prune* side — a differently-shaped-but-correct result is harmless. Ordering guarantees, human approval gates, platform behaviour a model cannot derive, and review policy fall on the *keep* side — variance there is a defect. Pulling back is **not** the same as removing constraints: everything that encodes a real rule stays exactly as it is. The skill authoring guidance in [`AGENTS.md`](AGENTS.md#skill-design-philosophy) applies this test in full, including the two categories — prompt-avoidance instructions and platform-behaviour notes — that read as noise but are load-bearing, and the reminder to calibrate against the weakest supported model, not the strongest. + ## Quick start Requires Python 3.8+ (stdlib only — no pip install needed). diff --git a/config.schema.yaml b/config.schema.yaml index b5d148b..219f289 100644 --- a/config.schema.yaml +++ b/config.schema.yaml @@ -108,6 +108,12 @@ placeholders: category: workflow used_in: [submit-for-review] + TEST_CMD: + description: "Command that runs the project's test suite during development, so /start's coding loop can point at it instead of the agent hand-rolling a test invocation. Optional and opt-in: empty (the default) means no separate test target exists, and /start degrades silently rather than inventing one — verification then rests on CHECK_CMD at the /submit-for-review gate. Set it (e.g. 'make test', 'npm test', 'pytest') only when the project has a real test command." + default: "" + category: workflow + used_in: [start] + MERGE_CMD: description: "Merge the current feature PR into the integration branch" default: "make merge" diff --git a/docs/config-reference.md b/docs/config-reference.md index a406c66..fb8e026 100644 --- a/docs/config-reference.md +++ b/docs/config-reference.md @@ -28,6 +28,7 @@ See [branching models](branching.md) for how these values change skill behavior. | Key | Default | Used in | Description | |---|---|---|---| | `CHECK_CMD` | `make check` | submit-for-review | Type-check / lint gate that must pass before shipping. | +| `TEST_CMD` | *(empty)* | start | Optional test-suite command `/start`'s coding loop points at instead of hand-rolling one. Empty (the default) means no separate test target — `/start` degrades silently and verification rests on `CHECK_CMD`. | | `DEV_CMD` | `make dev` | start | Start the local development server. Suggested to user after `/start` writes code. | | `ABANDON_CMD` | `make abandon` | start | Discard all changes and delete the current feature branch. | | `MERGE_CMD` | `make merge` | submit-for-review, deploy | Merge the current feature PR into the integration branch. | diff --git a/skills/github-agile/deploy.md b/skills/github-agile/deploy.md index 1cd3480..f596e60 100644 --- a/skills/github-agile/deploy.md +++ b/skills/github-agile/deploy.md @@ -7,50 +7,35 @@ args: none ## What `/deploy` does -`/deploy` is the final step in the workflow. It combines version bumping and release creation into a single command: check state, optionally bump the version, then create a GitHub Release (and in multi-branch mode, promote to production). +`/deploy` is the final step in the workflow. It combines version bumping and release creation into a single command: check state, optionally bump the version, then create a GitHub Release (and in multi-branch mode, promote the deploy branch to production first). + +The branching mode changes the shape of the release: in **trunk mode** (`BRANCH_PROD` only) `/deploy` tags and releases the current branch directly; in **multi-branch mode** (`BRANCH_DEV` set, optionally with `BRANCH_TEST`) it first opens and merges a release PR from the deploy branch into production, and that merge is what closes the linked issues. --- -## Step 1 — Verify branch +## Step 1 — Verify branch and sync -Run: -```bash -git branch --show-current -``` +Run `git branch --show-current`. The **deploy branch** for this project is: {{#if BRANCH_TEST}} -Required branch: `{{BRANCH_TEST}}` (three-branch mode). +`{{BRANCH_TEST}}` (three-branch mode). {{/if}} {{#if !BRANCH_TEST}} {{#if BRANCH_DEV}} -Required branch: `{{BRANCH_DEV}}` (two-branch mode). +`{{BRANCH_DEV}}` (two-branch mode). {{/if}} {{#if !BRANCH_DEV}} -Required branch: `{{BRANCH_PROD}}` (trunk mode). +`{{BRANCH_PROD}}` (trunk mode). {{/if}} {{/if}} -If not on the required branch, abort and say: "Switch to `<required-branch>` before running `/deploy`." +If not on the deploy branch, abort: "Switch to `<deploy-branch>` before running `/deploy`." -Sync to the remote before proceeding. The script below guards against uncommitted local changes, then runs `git checkout`, `git fetch`, and `git reset --hard origin/<base>` as one atomic operation. The deploy branch is never edited locally under the CodeCannon workflow (only fast-forwarded from CodeCannon's own merges), so the hard reset is the correct sync; the dirty-tree guard catches accidental local edits before they get silently discarded. +Then sync it to the remote. The script guards against uncommitted local changes, then runs `git checkout`, `git fetch`, and `git reset --hard origin/<deploy-branch>` as one atomic operation. The deploy branch is never edited locally under the CodeCannon workflow (only fast-forwarded from merges), so the hard reset is the correct sync; the dirty-tree guard catches accidental local edits before they are silently discarded. -{{#if BRANCH_TEST}} -```bash -python3 CodeCannon/skills/github-agile/scripts/sync-base-branch.py {{BRANCH_TEST}} -``` -{{/if}} -{{#if !BRANCH_TEST}} -{{#if BRANCH_DEV}} ```bash -python3 CodeCannon/skills/github-agile/scripts/sync-base-branch.py {{BRANCH_DEV}} +python3 CodeCannon/skills/github-agile/scripts/sync-base-branch.py <deploy-branch> ``` -{{/if}} -{{#if !BRANCH_DEV}} -```bash -python3 CodeCannon/skills/github-agile/scripts/sync-base-branch.py {{BRANCH_PROD}} -``` -{{/if}} -{{/if}} If the script exits non-zero, stop and resolve the issue it reports before continuing. @@ -58,112 +43,51 @@ If the script exits non-zero, stop and resolve the issue it reports before conti ## Step 2 — Check current state -### Find the latest version tag - -```bash -git describe --tags --abbrev=0 -``` - -If no tag exists, note this is the first release. - -### Read current version - -```bash -{{VERSION_READ_CMD}} -``` +Find the latest version tag (`git describe --tags --abbrev=0`; if none, note this is the first release) and read the current version with `{{VERSION_READ_CMD}}`. -### Show commits since last tag - -If a previous tag exists, show what's on the branch since that tag: +Show the merge commits (and their PRs) since the last tag. The range depends on the mode: {{#if !BRANCH_DEV}} ```bash git log <latest-tag>..HEAD --merges --pretty=format:"%s" ``` - -Parse PR numbers from merge commit subjects (format: `Merge pull request #N from branch/name`). {{/if}} {{#if BRANCH_DEV}} -{{#if !BRANCH_TEST}} ```bash -git log {{BRANCH_PROD}}..{{BRANCH_DEV}} --merges --pretty=format:"%s" +git log {{BRANCH_PROD}}..<deploy-branch> --merges --pretty=format:"%s" ``` - -Parse PR numbers from merge commit subjects (format: `Merge pull request #N from branch/name`). -{{/if}} {{#if BRANCH_TEST}} -```bash -git log {{BRANCH_PROD}}..{{BRANCH_TEST}} --merges --pretty=format:"%s" -``` - -Parse PR numbers from merge commit subjects. Note: some merge commits here may be promotion merges from `{{BRANCH_DEV}}` — these are identifiable by subjects matching "Merge ... from `{{BRANCH_DEV}}`". Include them in the list but note they are promotion merges; extract the original feature PRs from their PR bodies when possible. +Some merges here may be promotion merges from `{{BRANCH_DEV}}` (subjects matching "Merge ... from `{{BRANCH_DEV}}`"). Include them, but extract the original feature PRs from their PR bodies where possible. {{/if}} {{/if}} -For each PR number found, retrieve the PR body: -```bash -gh pr view <N> --json number,title,body -``` +Merge-commit subjects have the form `Merge pull request #N from branch/name` — parse the PR numbers, then retrieve each body with `gh pr view <N> --json number,title,body`. + +From those PR bodies, compile the release's issue links: {{#if !BRANCH_DEV}} -Extract `Closes #N` references from PR bodies. Compile: -- List of PRs included (number + title) -- List of issues linked to those PRs +- **PRs included** (number + title). +- **Issues linked** via `Closes #N`. {{/if}} {{#if BRANCH_DEV}} -Extract closing keywords **separately** from context references — do **not** merge them into a single set: - -- **Close set** — the union of every `Closes #N` line across all constituent PR bodies. These issues will auto-close when the release PR merges into `{{BRANCH_PROD}}`. Record, per constituent PR, the exact `Closes #N` lines it contained so they can be reproduced verbatim in the release PR body. -- **Reference set** — issues mentioned only via `Related to #N`, or via the legacy `Issue #N` form. These are context links and will **not** close. Legacy `Issue #N` carries no recoverable close-intent, so it stays in the reference set rather than being guessed into the close set; the HUMAN GATE surfaces it so you can manually close any straggler that should have been a `Closes`. +Keep closing keywords and context references **separate — do not merge them into one set**: -Compile: -- List of PRs included (number + title) -- Close set and reference set, kept distinct +- **Close set** — the union of every `Closes #N` line across all constituent PR bodies. These auto-close when the release PR merges into `{{BRANCH_PROD}}`. Record, per constituent PR, the exact `Closes #N` lines so they can be reproduced verbatim in the release PR body. +- **Reference set** — issues mentioned only via `Related to #N` or the legacy `Issue #N` form. These are context links and will **not** close. Legacy `Issue #N` carries no recoverable close-intent, so it stays in the reference set rather than being guessed into the close set; the human gate surfaces it so you can manually close any straggler that should have closed. +- **PRs included** (number + title). {{/if}} -### Check for open unmerged PRs - -```bash -gh pr list --state open --json number,title,headRefName -``` - -### Present the summary - -Tell the user: - -``` -Current version: X.Y.Z -Latest tag: vX.Y.Z - -Commits/PRs since last tag: - #17 — Add /docs directory - #18 — Fix checkout runtime error - -Open PRs not yet merged: - #19 — Add dark mode (feature/dark-mode) - -Would you like to bump the version before deploying? - - **patch** → X.Y.C - - **minor** → X.B.0 - - **major** → A.0.0 - - **specific** → enter a version number - - **skip** → proceed to release with the latest existing tag -``` +Also check for open unmerged PRs (`gh pr list --state open --json number,title,headRefName`). -Wait for their response. +Present a summary — current version, latest tag, the PRs/issues since that tag, any open PRs — and ask whether to bump the version before deploying (patch → X.Y.C, minor → X.B.0, major → A.0.0, a specific version, or skip to release the latest existing tag). Wait for their response. --- ## Step 3 — Version bump (if requested) -If the user chose to skip, find the latest version tag in the branch history: -```bash -git describe --tags --abbrev=0 -``` - -If no tag is found at all (first release), warn: "No version tag found. You must bump the version before deploying." Return to the version bump prompt. Otherwise, use the tag found as the release version. +If the user chose **skip**, use the latest existing tag (`git describe --tags --abbrev=0`) as the release version. If none exists (first release), warn "No version tag found. You must bump the version before deploying." and return to the bump prompt. -If the user chose a bump level, map their response to a bump command and run `bump-and-tag.py`, which performs the bump, verifies the resulting tag (creating an annotated fallback if `tag.forceSignAnnotated` silently rejected a lightweight tag), and pushes both the commit and the tag. The resolved version is printed on stdout — capture it for the release step. +If the user chose a bump level, map it to a bump command and run `bump-and-tag.py`, which performs the bump, verifies the resulting tag (creating an annotated fallback if `tag.forceSignAnnotated` silently rejected a lightweight tag), and pushes both the commit and the tag. The resolved version is printed on stdout — capture it as `<new-version>`. | User says | `--bump-cmd` | |---|---| @@ -178,154 +102,55 @@ python3 CodeCannon/skills/github-agile/scripts/bump-and-tag.py \ --version-read-cmd "{{VERSION_READ_CMD}}" ``` -If the script exits non-zero, stop and resolve the issue it reports before continuing. On success, the version printed on stdout is the new version — use it as `<new-version>` in subsequent steps. +If the script exits non-zero, stop and resolve the issue it reports before continuing. --- ## Step 4 — Compute release contents -Determine the version tag (either from the bump just performed, or from the existing HEAD tag if the user skipped bumping). - -Find the previous tag to determine the range: -```bash -git describe --abbrev=0 <version-tag>^ -``` +Determine the release version tag (from the bump just performed, or the existing HEAD tag if the user skipped). Find the previous tag for the changelog range: `git describe --abbrev=0 <version-tag>^`. {{#if !BRANCH_DEV}} -Find all merge commits since the previous tag: -```bash -git log <prev-tag>..HEAD --merges --pretty=format:"%s" -``` - -Parse PR numbers from merge commit subjects (format: `Merge pull request #N from branch/name`). - -For each PR number found, retrieve the PR body: -```bash -gh pr view <N> --json number,title,body -``` - -Extract `Closes #N` references from PR bodies (trunk PRs use `Closes #N`). Compile: -- List of PRs included (number + title) -- List of issues linked via `Closes #N` +Find the merge commits since the previous tag (`git log <prev-tag>..HEAD --merges --pretty=format:"%s"`), parse their PR numbers, retrieve each body (`gh pr view <N> --json number,title,body`), and compile the PRs included plus the issues linked via `Closes #N`. {{/if}} {{#if BRANCH_DEV}} -{{#if !BRANCH_TEST}} -Use the PR list, close set, and reference set already computed in Step 2. If the version bump added new commits, re-fetch if needed. -{{/if}} -{{#if BRANCH_TEST}} -Use the PR list, close set, and reference set already computed in Step 2. If the version bump added new commits, re-fetch if needed. -{{/if}} +Reuse the PR list, close set, and reference set already computed in Step 2. If the version bump added new commits, re-fetch as needed. {{/if}} --- ## Step 5 — HUMAN GATE -Show the user the release summary. Example format: - -``` -Ready to release vX.Y.Z to production. - -PRs included: - #17 — Add /docs directory - #18 — Fix checkout runtime error +Show the release summary — the target version, the PRs included, and the issue links: {{#if !BRANCH_DEV}} -Issues that will be referenced: - #14 — Add /docs directory - #15 — Fix checkout runtime error -{{/if}} -{{#if BRANCH_DEV}} -Issues that will close on merge (Closes #N, reproduced verbatim from constituent PRs): - #14 — Add /docs directory - #15 — Fix checkout runtime error +- Issues that will be referenced. -Issues referenced but NOT closing (Related to #N / legacy Issue #N — confirm none of these should actually close): - #20 — Tighten error copy on the upload form +Confirm production readiness: "Have you confirmed everything above is ready for production? Type 'release' to confirm." {{/if}} +{{#if BRANCH_DEV}} +- Issues that will **close** on merge (the close set, reproduced verbatim from constituent PRs). +- Issues **referenced but not closing** (the reference set — confirm none of these should actually close). -{{#if !BRANCH_DEV}} -Have you confirmed everything above is ready for production? Type 'release' to confirm. +Confirm the deploy branch has been tested: +{{#if BRANCH_TEST}} +"Have you tested all of the above on the {{BRANCH_TEST}} environment? Type 'release' to confirm." {{/if}} -{{#if BRANCH_DEV}} {{#if !BRANCH_TEST}} -Have you tested all of the above on preview? Type 'release' to confirm. -{{/if}} -{{#if BRANCH_TEST}} -Have you tested all of the above on the {{BRANCH_TEST}} environment? Type 'release' to confirm. +"Have you tested all of the above on preview? Type 'release' to confirm." {{/if}} {{/if}} -``` - -Wait for the user to type "release" or an explicit confirmation. Any other response → stop and ask what they'd like to change. - ---- -{{#if !BRANCH_DEV}} -## Step 6 — Create GitHub Release - -**Publish confirmation — required before writing the release notes or creating the Release.** The GitHub Release is a public-surface action. The single word from Step 5 authorizes the promotion/merge; the public publish gets its own explicit confirmation. Tell the user (substitute the actual tag, e.g. `v0.13.0`): - -> Publishing GitHub Release `<version-tag>` — the final public step. Confirm by pasting: `publish <version-tag>` - -Wait for the user to paste `publish <version-tag>` (or an explicit version-named variant such as `ship <version-tag>`). Any other response → stop and ask what they'd like to change. The version-named phrase is deliberate: Claude Code's auto-mode safety classifier requires authorization that names the release before `gh release create` runs, so the generic Step 5 confirmation is not relied on for the public publish. If a harness still blocks the call after this confirmation (e.g. an older client), the user can re-confirm with `publish <version-tag> release` to unblock. +Wait for "release" or an explicit confirmation. Any other response → stop and ask what they'd like to change. --- -The version tag and PR/issue list are already known. If no previous tag exists, omit the "Full changelog" line. - -First, create a temp directory for this invocation: - -```bash -python3 CodeCannon/skills/github-agile/scripts/make-workdir.py -``` - -Note the returned path (e.g. `/tmp/CodeCannon/a8f3b2`). Use this path for all temp files in this invocation. - -Then use your file-writing tool (not Bash) to create `<tmpdir>/release_notes.md`: - -```markdown -## Changes - -- #<issue> — <PR title> (PR #<pr-number>) -[... one line per PR included in this release ...] - -**Full changelog:** https://github.com/<owner>/<repo>/compare/<previous-tag>...<version-tag> -``` - -Then create the release (do NOT use `--notes`, `--notes-file -`, or heredocs): - -```bash -gh release create <version-tag> \ - --title "<version-tag>" \ - --notes-file <tmpdir>/release_notes.md -``` - -Format each PR line as `- #<linked-issue> — <PR title> (PR #<N>)`. If a PR had no linked issue, use just the PR title. - -After the command runs, note the release URL from the output. - ---- - -## Step 7 — Report - -Tell the user: - -> "Released vX.Y.Z. Issues closed on merge. GitHub Release vX.Y.Z created at `<url>`. Run `{{DEPLOY_PROD_CMD}}` to ship to production." -{{/if}} {{#if BRANCH_DEV}} -{{#if !BRANCH_TEST}} -## Step 6 — Create PR: `{{BRANCH_DEV}}` → `{{BRANCH_PROD}}` - -First, create a temp directory for this invocation: - -```bash -python3 CodeCannon/skills/github-agile/scripts/make-workdir.py -``` +## Step 6 — Promote: `<deploy-branch>` → `{{BRANCH_PROD}}` -Note the returned path (e.g. `/tmp/CodeCannon/a8f3b2`). Use this path for all temp files in this invocation. +Create a temp directory for this invocation (`python3 CodeCannon/skills/github-agile/scripts/make-workdir.py`) and note the returned path — use it for all temp files here. -Then use your file-writing tool (not Bash) to create `<tmpdir>/release_pr_body.md`: +Use your file-writing tool (not Bash) to create `<tmpdir>/release_pr_body.md`: ```markdown Release vX.Y.Z @@ -340,156 +165,37 @@ Closes #15 Related to #20 ``` -Reproduce **every** `Closes #N` line from the close set computed in Step 2 — verbatim, one per line, omitting none. Add a `Related to #N` line for each issue in the reference set so the links appear on the release PR without triggering an auto-close. If the reference set is empty, omit the `Related to` lines entirely. +Reproduce **every** `Closes #N` line from the close set — verbatim, one per line, omitting none. Add a `Related to #N` line for each issue in the reference set so the links appear without triggering an auto-close; if the reference set is empty, omit the `Related to` lines entirely. -Then create the PR (do NOT use `--body`, `--body-file -`, or heredocs): +Create the PR (do NOT use `--body`, `--body-file -`, or heredocs), with `--head` set to the deploy branch: ```bash -gh pr create --base {{BRANCH_PROD}} --head {{BRANCH_DEV}} \ +gh pr create --base {{BRANCH_PROD}} --head <deploy-branch> \ --title "Release vX.Y.Z" \ --body-file <tmpdir>/release_pr_body.md ``` -Note the PR number from the output. - -The `Closes #N` lines will auto-close the linked issues because this PR merges into `{{BRANCH_PROD}}` (the default branch). - -> **Critical:** Use the unqualified `#N` form only. Never write `Closes owner/repo#N`, even for same-repo refs — GitHub's closing-keyword parser only populates `closingIssuesReferences` for the unqualified form, and the qualified form silently breaks auto-close. - ---- - -## Step 7 — Merge - -Do NOT use `{{MERGE_CMD}}` — it refuses PRs targeting `{{BRANCH_PROD}}`. Use `gh pr merge` directly: - -```bash -gh pr merge <pr-number> --merge -``` - ---- - -## Step 8 — Create GitHub Release - -**Publish confirmation — required before writing the release notes or creating the Release.** The GitHub Release is a public-surface action. The single word from Step 5 authorized the promotion/merge (already done); the public publish gets its own explicit confirmation. Tell the user (substitute the actual tag, e.g. `v0.13.0`): +> **Critical:** Use the unqualified `#N` form only. Never write `Closes owner/repo#N`, even for same-repo refs — GitHub's closing-keyword parser only populates `closingIssuesReferences` for the unqualified form, and the qualified form silently breaks auto-close. The `Closes #N` lines auto-close the linked issues because this PR merges into `{{BRANCH_PROD}}` (the default branch). -> Publishing GitHub Release `<version-tag>` — the final public step. Confirm by pasting: `publish <version-tag>` - -Wait for the user to paste `publish <version-tag>` (or an explicit version-named variant such as `ship <version-tag>`). Any other response → stop and ask what they'd like to change. The version-named phrase is deliberate: Claude Code's auto-mode safety classifier requires authorization that names the release before `gh release create` runs, so the generic Step 5 confirmation is not relied on for the public publish. If a harness still blocks the call after this confirmation (e.g. an older client), the user can re-confirm with `publish <version-tag> release` to unblock. +Then merge. Do NOT use `{{MERGE_CMD}}` — it refuses PRs targeting `{{BRANCH_PROD}}`. Use `gh pr merge <pr-number> --merge` directly. --- - -The version tag (from Step 3) and the PR/issue list (from Step 4) are already known. Find the previous tag to build the changelog link: - -```bash -git describe --abbrev=0 <version-tag>^ -``` - -If no previous tag exists, omit the "Full changelog" line. - -Use your file-writing tool (not Bash) to create `<tmpdir>/release_notes.md` (same temp directory from Step 6): - -```markdown -## Changes - -- #<issue> — <PR title> (PR #<pr-number>) -[... one line per PR included in this release ...] - -**Full changelog:** https://github.com/<owner>/<repo>/compare/<previous-tag>...<version-tag> -``` - -Then create the release (do NOT use `--notes`, `--notes-file -`, or heredocs): - -```bash -gh release create <version-tag> \ - --title "<version-tag>" \ - --notes-file <tmpdir>/release_notes.md -``` - -Format each PR line as `- #<linked-issue> — <PR title> (PR #<N>)`. If a PR had no linked issue, omit the `#<issue>` prefix and use just the PR title. - -After the command runs, note the release URL from the output. - ---- - -## Step 9 — Report - -Tell the user: - -> "Released vX.Y.Z. Issues #N, #M closed automatically. GitHub Release vX.Y.Z created at `<url>`. Run `{{DEPLOY_PROD_CMD}}` to ship to production." {{/if}} -{{#if BRANCH_TEST}} -## Step 6 — Create PR: `{{BRANCH_TEST}}` → `{{BRANCH_PROD}}` - -First, create a temp directory for this invocation: -```bash -python3 CodeCannon/skills/github-agile/scripts/make-workdir.py -``` - -Note the returned path (e.g. `/tmp/CodeCannon/a8f3b2`). Use this path for all temp files in this invocation. - -Then use your file-writing tool (not Bash) to create `<tmpdir>/release_pr_body.md`: - -```markdown -Release vX.Y.Z - -PRs included: -- #17 — Add /docs directory -- #18 — Fix checkout runtime error - -Closes #14 -Closes #15 - -Related to #20 -``` - -Reproduce **every** `Closes #N` line from the close set computed in Step 2 — verbatim, one per line, omitting none. Add a `Related to #N` line for each issue in the reference set so the links appear on the release PR without triggering an auto-close. If the reference set is empty, omit the `Related to` lines entirely. - -Then create the PR (do NOT use `--body`, `--body-file -`, or heredocs): - -```bash -gh pr create --base {{BRANCH_PROD}} --head {{BRANCH_TEST}} \ - --title "Release vX.Y.Z" \ - --body-file <tmpdir>/release_pr_body.md -``` - -Note the PR number from the output. - -The `Closes #N` lines will auto-close the linked issues because this PR merges into `{{BRANCH_PROD}}` (the default branch). - -> **Critical:** Use the unqualified `#N` form only. Never write `Closes owner/repo#N`, even for same-repo refs — GitHub's closing-keyword parser only populates `closingIssuesReferences` for the unqualified form, and the qualified form silently breaks auto-close. - ---- - -## Step 7 — Merge - -Do NOT use `{{MERGE_CMD}}` — it refuses PRs targeting `{{BRANCH_PROD}}`. Use `gh pr merge` directly: - -```bash -gh pr merge <pr-number> --merge -``` - ---- - -## Step 8 — Create GitHub Release +{{#if BRANCH_DEV}} +## Step 7 — Create the GitHub Release +{{/if}} +{{#if !BRANCH_DEV}} +## Step 6 — Create the GitHub Release +{{/if}} -**Publish confirmation — required before writing the release notes or creating the Release.** The GitHub Release is a public-surface action. The single word from Step 5 authorized the promotion/merge (already done); the public publish gets its own explicit confirmation. Tell the user (substitute the actual tag, e.g. `v0.13.0`): +**Publish confirmation — required before writing the release notes or creating the Release.** The GitHub Release is a public-surface action; the confirmation from Step 5 authorized the promotion, but the public publish gets its own explicit confirmation. Tell the user (substitute the actual tag, e.g. `v0.13.0`): > Publishing GitHub Release `<version-tag>` — the final public step. Confirm by pasting: `publish <version-tag>` Wait for the user to paste `publish <version-tag>` (or an explicit version-named variant such as `ship <version-tag>`). Any other response → stop and ask what they'd like to change. The version-named phrase is deliberate: Claude Code's auto-mode safety classifier requires authorization that names the release before `gh release create` runs, so the generic Step 5 confirmation is not relied on for the public publish. If a harness still blocks the call after this confirmation (e.g. an older client), the user can re-confirm with `publish <version-tag> release` to unblock. ---- - -The version tag (from Step 3) and the PR/issue list (from Step 4) are already known. Find the previous tag to build the changelog link: - -```bash -git describe --abbrev=0 <version-tag>^ -``` - -If no previous tag exists, omit the "Full changelog" line. - -Use your file-writing tool (not Bash) to create `<tmpdir>/release_notes.md` (same temp directory from Step 6): +The version tag and PR/issue list are already known; the previous tag comes from Step 4 (if there is no previous tag, omit the "Full changelog" line). Create a temp directory if you haven't already (`python3 CodeCannon/skills/github-agile/scripts/make-workdir.py`), then use your file-writing tool (not Bash) to create `<tmpdir>/release_notes.md`: ```markdown ## Changes @@ -500,7 +206,7 @@ Use your file-writing tool (not Bash) to create `<tmpdir>/release_notes.md` (sam **Full changelog:** https://github.com/<owner>/<repo>/compare/<previous-tag>...<version-tag> ``` -Then create the release (do NOT use `--notes`, `--notes-file -`, or heredocs): +Format each PR line as `- #<linked-issue> — <PR title> (PR #<N>)`; if a PR had no linked issue, use just the PR title. Then create the release (do NOT use `--notes`, `--notes-file -`, or heredocs): ```bash gh release create <version-tag> \ @@ -508,16 +214,15 @@ gh release create <version-tag> \ --notes-file <tmpdir>/release_notes.md ``` -Format each PR line as `- #<linked-issue> — <PR title> (PR #<N>)`. If a PR had no linked issue, omit the `#<issue>` prefix and use just the PR title. - -After the command runs, note the release URL from the output. +Note the release URL from the output. --- -## Step 9 — Report - -Tell the user: - -> "Released vX.Y.Z. Issues #N, #M closed automatically. GitHub Release vX.Y.Z created at `<url>`. Run `{{DEPLOY_PROD_CMD}}` to ship to production." +{{#if BRANCH_DEV}} +## Step 8 — Report {{/if}} +{{#if !BRANCH_DEV}} +## Step 7 — Report {{/if}} + +Tell the user: "Released vX.Y.Z. Linked issues are closed. GitHub Release vX.Y.Z created at `<url>`. Run `{{DEPLOY_PROD_CMD}}` to ship to production." diff --git a/skills/github-agile/start.md b/skills/github-agile/start.md index 3213366..104905e 100644 --- a/skills/github-agile/start.md +++ b/skills/github-agile/start.md @@ -25,17 +25,15 @@ Otherwise → go to **Case A: New work**. > **Execution order:** Resolve labels and milestones **now**, before entering Case A Step 1. If milestone auto-detection requires a user prompt (2+ open milestones), that prompt happens here — not later during issue creation. By the time you reach Step 2's human gate, all metadata must already be resolved so that Step 3 can proceed without re-prompting. -The argument string may contain optional inline flags after the description. Parse as follows: +The description may be followed by optional flags — `--label`/`-l` and `--milestone`/`-m`, in any order. Separate the description from the flags yourself; the flags carry these meanings: -1. **Identify flags** — scan for the first token that starts with `--label`, `-l`, `--milestone`, or `-m`. Everything before it is the **description**. Everything from the first flag onward is **flags**. -2. **`--label <value>` / `-l <value>`** — comma-separated label string (e.g. `bug` or `enhancement,ux`). If provided, it **bypasses label auto-selection entirely** for this invocation — use the value verbatim. Labels containing spaces must be quoted (e.g. `--label "good first issue"`). +- **`--label <value>` / `-l <value>`** — a comma-separated label string used **verbatim**, bypassing label auto-selection entirely for this invocation. Quote values containing spaces (e.g. `--label "good first issue"`). {{#if DEFAULT_MILESTONE}} -3. **`--milestone <value>` / `-m <value>`** — milestone name or number (e.g. `Sprint 4` or `12`). If provided, it **replaces** the default milestone `{{DEFAULT_MILESTONE}}` for this invocation. Pass the value as-is; GitHub accepts both names and numbers. +- **`--milestone <value>` / `-m <value>`** — a milestone name or number that **replaces** the default milestone `{{DEFAULT_MILESTONE}}` for this invocation (GitHub accepts both names and numbers). {{/if}} {{#if !DEFAULT_MILESTONE}} -3. **`--milestone <value>` / `-m <value>`** — milestone name or number (e.g. `Sprint 4` or `12`). Pass the value as-is; GitHub accepts both names and numbers. +- **`--milestone <value>` / `-m <value>`** — a milestone name or number (GitHub accepts both names and numbers). {{/if}} -4. **Flags may appear in any order** after the description. **Label resolution (three-tier, Case A only):** @@ -81,33 +79,6 @@ After parsing flags, determine the active milestone in this order: - **1 result** → use its title silently. Inform the user inline: `(milestone: <title>)`. - **2+ results** → show the numbered list, ask once: **"Multiple open milestones — which should this issue go under? (enter a number or title, or 'none')"**. Accept milestone number, title, or "none"/"skip". Wait for response before continuing. -**Examples:** - -{{#if TICKET_LABELS}} -{{#if DEFAULT_MILESTONE}} -| `$ARGUMENTS` | Description | Labels | Milestone | -|---|---|---|---| -| `Add dark mode toggle to settings page` | `Add dark mode toggle to settings page` | auto-selected from pool | `{{DEFAULT_MILESTONE}}` | -| `Add dark mode --label enhancement` | `Add dark mode` | `enhancement` (verbatim) | `{{DEFAULT_MILESTONE}}` | -| `Add dark mode --label enhancement,ux --milestone "Sprint 4"` | `Add dark mode` | `enhancement,ux` (verbatim) | `Sprint 4` | -| `Add dark mode --milestone sprint-4` | `Add dark mode` | auto-selected from pool | `sprint-4` | -{{/if}} -{{#if !DEFAULT_MILESTONE}} -| `$ARGUMENTS` | Description | Labels | Milestone | -|---|---|---|---| -| `Add dark mode toggle to settings page` | `Add dark mode toggle to settings page` | auto-selected from pool | auto-detected | -| `Add dark mode --label enhancement` | `Add dark mode` | `enhancement` (verbatim) | auto-detected | -| `Add dark mode --label enhancement,ux --milestone "Sprint 4"` | `Add dark mode` | `enhancement,ux` (verbatim) | `Sprint 4` | -{{/if}} -{{/if}} -{{#if !TICKET_LABELS}} -| `$ARGUMENTS` | Description | Labels | Milestone | -|---|---|---|---| -| `Add dark mode toggle to settings page` | `Add dark mode toggle to settings page` | none (no label pool) | auto-detected | -| `Add dark mode --label enhancement` | `Add dark mode` | `enhancement` (verbatim) | auto-detected | -| `Add dark mode --label enhancement,ux --milestone "Sprint 4"` | `Add dark mode` | `enhancement,ux` (verbatim) | `Sprint 4` | -{{/if}} - > Replace vs append: flags **replace** auto-selection entirely, they do not append. This avoids silent label duplication and milestone conflicts. --- @@ -145,7 +116,7 @@ If on any other branch → proceed to Case A or Case B as determined by the `$AR ### Step 1 — Investigate -Read the relevant code. Propose a concrete implementation approach. Be specific about which files change and how. +Read the relevant code using your harness's native file-reading and search tools (read, grep/glob, and the like) rather than shell pipelines — hand-rolled `find … | xargs`, `grep ; awk`, or redirection shapes trigger permission prompts that cannot be permanently allowed. Then propose a concrete implementation approach, specific about which files change and how. ### Step 2 — HUMAN GATE @@ -281,7 +252,19 @@ Show the user: `On branch feature/<name>` ### Step 5 — Write the code -Now write the code. Do NOT commit anything. +Write the code using your harness's native editing tools. Do NOT commit anything. +{{#if TEST_CMD}} +To exercise your work as you go, run the project's configured test command rather than hand-rolling a test invocation — a hand-built `python3 -m unittest … > /tmp/…` or similar redirection shape triggers permission prompts that a configured command avoids: + +```bash +{{TEST_CMD}} +``` + +If it fails because the target does not exist, tell the user rather than improvising a replacement. +{{/if}} +{{#if !TEST_CMD}} +No project test command is configured, so do not invent one to run here — verification happens at the check gate inside `/submit-for-review`. +{{/if}} When done, say: **"When you've verified locally, reply `yes` to submit, or say what to change."** @@ -392,7 +375,19 @@ git branch --show-current ### Step 5 — Write the code -Continue from where work left off. Do NOT commit. +Continue from where work left off, using your harness's native editing tools. Do NOT commit. +{{#if TEST_CMD}} +To exercise your work as you go, run the project's configured test command rather than hand-rolling a test invocation — a hand-built `python3 -m unittest … > /tmp/…` or similar redirection shape triggers permission prompts that a configured command avoids: + +```bash +{{TEST_CMD}} +``` + +If it fails because the target does not exist, tell the user rather than improvising a replacement. +{{/if}} +{{#if !TEST_CMD}} +No project test command is configured, so do not invent one to run here — verification happens at the check gate inside `/submit-for-review`. +{{/if}} When done, say: **"When you've verified locally, reply `yes` to submit, or say what to change."** diff --git a/skills/github-agile/status.md b/skills/github-agile/status.md index 62c7bb4..7d20e93 100644 --- a/skills/github-agile/status.md +++ b/skills/github-agile/status.md @@ -4,45 +4,39 @@ description: "Code Cannon: Summarize in-progress and recently completed work fro args: "optional: lookback days (e.g. 14), a GitHub username (@alice), --milestone <name>, or --team" --- -## Step 1 — Parse arguments +## What `/status` does -First, check whether `$ARGUMENTS` contains `--milestone`, `--sprint`, or `--team`. +`/status` prints a read-only, standup-ready snapshot of in-progress and recently completed work, then a single "what's next" suggestion. It never writes to GitHub or the working tree — it only reads and reports. -**Milestone mode:** If `--milestone` or `--sprint` is present, extract everything after the flag as the milestone name (trim leading/trailing whitespace; preserve internal spaces). Ignore any other arguments. Enter milestone mode (Steps M1–M3 below) and skip Steps 2–6. +Because it is read-only, the *shape* of its output does not matter: a differently-formatted-but-accurate summary is a fine result. Derive a clear, scannable layout yourself. What this skill pins down is the data to fetch, how to classify it, and the one piece of real opinion — the "what's next" ordering. -Examples: -- `--milestone Sprint 4` → milestone name = `Sprint 4` -- `--sprint Sprint 4` → milestone name = `Sprint 4` -- `--milestone Q2 Release` → milestone name = `Q2 Release` -- `--milestone 12` → milestone name = `12` - -**Team mode:** If `--team` is present, enter team mode (Steps T1–T3 below) and skip Steps 2–6. `--team` is mutually exclusive with `--milestone`/`--sprint` and username arguments. If both are present, report the conflict and stop. +--- -**Personal mode** (no `--milestone` / `--sprint` / `--team` flag): determine: +## Step 1 — Determine mode -- **subject**: default `@me`. If the argument starts with `@` or is a plain word that is not a number, treat it as a GitHub username. Strip the leading `@` for `gh` commands that do not accept it (e.g. `gh pr list --author alice`); keep it for display. -- **lookback**: default `7`. If the argument is a number (digits only), use it as the lookback window in days. +Three mutually exclusive modes, selected from `$ARGUMENTS`: -No argument → subject = `@me`, lookback = `7`. +- **Milestone mode** — `--milestone` or `--sprint` is present. Everything after the flag is the milestone name (a name or a number; trim outer whitespace, preserve internal spaces). Ignore other arguments. Run Steps M1–M2. +- **Team mode** — `--team` is present. Run Steps T1–T2. `--team` is mutually exclusive with `--milestone`/`--sprint` and with a username; if combined, report the conflict and stop. +- **Personal mode** — no mode flag. **Subject** defaults to `@me`; a `@name` or a non-numeric word is a username (strip the leading `@` for `gh` flags that reject it, keep it for display). **Lookback** defaults to `7`; a bare number is the lookback in days. --- -## Step 2 — Fetch GitHub data (run all in parallel) +## Step 2 — Fetch GitHub data (personal mode) -Run these commands concurrently: +Run these concurrently. If any `gh` command exits non-zero (including auth errors), report the message and stop — do not retry. -**Open PRs authored by subject:** +**Open PRs authored by subject** — request enough fields to derive health (draft, CI, review decision, merge conflict) and staleness: ```bash gh pr list --author <subject> --state open \ --json number,title,url,labels,milestone,baseRefName,body,reviewDecision,statusCheckRollup,updatedAt,mergeable,isDraft ``` -**Recently merged PRs (last `<lookback>` days):** +**Recently merged PRs**, filtered to those merged within `<lookback>` days: ```bash gh pr list --author <subject> --state merged --limit 20 \ --json number,title,url,mergedAt,labels,baseRefName ``` -Filter the results to keep only entries where `mergedAt` is within the last `<lookback>` days. **Open issues assigned to subject:** ```bash @@ -50,316 +44,102 @@ gh issue list --assignee <subject> --state open \ --json number,title,url,labels,milestone,updatedAt ``` -**PRs requesting your review** (only when subject is `@me`): +**PRs requesting your review** — only when subject is `@me`; skip for other users: ```bash gh pr list --search "review-requested:@me" --state open \ --json number,title,url,author,updatedAt ``` -Skip this query when viewing another user's status. -If any `gh` command exits with a non-zero status (including auth errors), report the error message and stop. Do not retry. - ---- - -## Step 3 — Fetch local git context - -Check if the current directory is inside a git repository: -```bash -git rev-parse --is-inside-work-tree -``` - -If yes, run: +Also fetch local git context (skip and note if not in a git repo — `git rev-parse --is-inside-work-tree`): ```bash git log --oneline --since="<lookback> days ago" ``` -If not inside a git repo, skip this step and note it was skipped in the output. - --- -## Step 4 — Classify items - -Using the data from Steps 2 and 3, classify each item: - -- **In progress** — open PRs. For each, attempt to identify a linked issue number from the PR body (look for `#N`, `closes #N`, `fixes #N`, `issue #N`). If found, cross-reference with open issues. -- **Done** — merged PRs within the lookback window. -- **Up next** — open issues that are NOT associated with any open PR (i.e. no open PR body references their issue number). -- **Needs your review** — PRs from the review-requested query (only when subject is `@me`). - -An open issue that IS linked from an open PR body appears under "In progress" alongside that PR, not under "Up next". +## Step 3 — Classify and report (personal mode) -### 4a — Derive health badges +Sort items into these buckets: -For each open PR, derive the following badges: +- **In progress** — open PRs. Identify a linked issue from the PR body (`#N`, `closes #N`, `fixes #N`, `issue #N`) and cross-reference open issues. +- **Done** — PRs merged within the lookback window. +- **Up next** — open issues whose number is **not** referenced by any open PR body. (An issue linked from an open PR belongs under *In progress* with that PR, not here.) +- **Needs your review** — the review-requested query (only when subject is `@me`). -**Draft status:** -- If `isDraft` is `true` → `[draft]` +For each open PR, derive health from the JSON — draft state, CI status from `statusCheckRollup`, review state from `reviewDecision`, merge conflict from `mergeable`. Present each as a compact badge; omit a badge when it does not apply or is not configured. -**CI check status** (from `statusCheckRollup`): -- All checks have `status: COMPLETED` and `conclusion: SUCCESS` → `✅ checks passing` -- Any check has `conclusion: FAILURE` → `❌ checks failing` -- Checks are still running or have other states → `⏳ checks pending` -- No checks configured → omit badge +**Staleness:** flag any open PR or issue not updated within `{{STALE_DAYS}}` days (a threshold of `0` disables staleness entirely). Note the last-updated date and age. This is a real config-driven rule — honor the threshold exactly. -**Review decision** (from `reviewDecision`): -- `APPROVED` → `✅ approved` -- `CHANGES_REQUESTED` → `🔄 changes requested` -- `REVIEW_REQUIRED` or empty → `⏳ awaiting review` +Report the buckets as a scannable summary: a heading naming the subject and lookback, a one-line count roll-up, then a section per non-empty bucket, then the local commits (or a note that git was skipped). Show labels/milestone only when present; dates as `YYYY-MM-DD`. If every GitHub bucket is empty, say so plainly for the subject and window. -**Merge conflict** (from `mergeable`): -- `CONFLICTING` → `⚠️ conflicts` -- `MERGEABLE` or `UNKNOWN` → omit badge - -### 4b — Flag stale items - -For each open PR and open issue, check `updatedAt`. If the item has not been updated within `{{STALE_DAYS}}` days (default: 14; disabled when set to 0), flag it as stale. Record the last-updated date and the number of days since the last update. - -A stale item gets an inline `⚠️ stale (<N>d)` badge appended after any other badges. +**Do not post, comment, write files, or take any action. Output only.** --- -## Step 5 — Output the summary - -Print a formatted summary. Use this structure: - -``` -## Status for <subject> — last <lookback> days - -<N> in progress · <N> done · <N> up next[ · <N> need your review] - -### In progress -- #<number> <title> [<labels>] [<milestone>] [draft] - PR: <url> · <check badge> · <review badge>[ · <conflict badge>][ · <stale badge>] - Linked issue: #<number> (if found) +## Step 4 — What's next (personal mode) -### Done -- #<number> <title> [<labels>] — merged <date> - PR: <url> +After the summary, append **one** actionable suggestion. Gather the extra local state you need (current branch, `git status --porcelain`, latest tag via `git describe --tags --abbrev=0`, unreleased commit count via `git rev-list <tag>..HEAD --count`, and the current branch's PR review/check state via `gh pr view` — treat a non-zero exit as "no PR for this branch"). Skip git lookups when not in a repo. -### Needs your review -- #<number> <title> (by @<author>) - PR: <url> +Evaluate these conditions **in order** and use the **first** match. This ordering is the workflow's opinion about what the operator should do next — it is load-bearing, not formatting: -### Up next -- #<number> <title> [<labels>] [<milestone>][ · <stale badge>] - Issue: <url> +| Priority | Condition | Suggestion | +|----------|-----------|------------| +| 1 | On a `feature/*` branch with uncommitted changes | You have uncommitted changes on `<branch>`. When ready, run `/submit-for-review`. | +| 2 | On a `feature/*` branch, open PR is `APPROVED` and all checks `COMPLETED` | PR #<number> is approved and checks pass. Consider running `/deploy`. | +| 3 | On a `feature/*` branch with an open PR in any other state | PR #<number> (<title>) is open and awaiting review. | +| 3.5 | Subject is `@me` and PRs request your review | Append to the current suggestion (or stand alone if nothing higher matched): You also have <N> PR(s) awaiting your review. | +| 4 | On a `feature/*` branch, no open PR, clean tree | No open PR for `<branch>`. Run `/submit-for-review` to open one. | +| 5 | On the integration branch with unreleased commits since the last tag | <N> commit(s) on `<branch>` since `<tag>`. Run `/deploy` when ready to release. | +| 6 | No open PRs and no open issues assigned to subject | Nothing in progress. Run `/start` to begin new work. | +| 7 | Open issues exist in "Up next" | Next up is #<number> (<title>). Run `/start <number>` to pick it up. | -### ⚠️ Stale -- #<number> <title> — last updated <date> (<N> days ago) +If none match, omit the "what's next" section. Omit it entirely in milestone mode. --- -Local commits (current branch): -<git log output, or "skipped — not in a git repo"> -``` -Rules: -- **Summary counts line**: show immediately after the heading. Omit zero-count segments (e.g., if nothing is done, skip that segment). "need your review" only appears when subject is `@me` and the count is > 0. -- **Health badges**: show on the second line of each "In progress" item, after the PR URL, separated by ` · `. Omit individual badges that don't apply (e.g., no conflict badge if mergeable). -- **Draft badge**: show `[draft]` inline in the first line of draft PRs, before any other badges. -- **Stale section**: a dedicated section at the bottom (before "Local commits") listing all stale items from any section, with their last-updated date and age. This gives a consolidated view. Individual items also get the inline `⚠️ stale (<N>d)` badge in their own sections. -- **"Needs your review" section**: only shown when subject is `@me` and there are PRs requesting review. Placed between "Done" and "Up next". -- Omit any section that has no items — do not show an empty heading. -- Show labels only if present; show milestone only if present. -- Dates use `YYYY-MM-DD` format. -- If all GitHub sections are empty, print: `Nothing found for <subject> in the last <lookback> days.` - -Do not post, comment, write files, or take any action. Output only. - ---- - -## Step 6 — What's next - -After the status summary, append a single actionable suggestion based on local git state and the GitHub data already fetched. - -### 6a — Gather additional local state - -Run these commands (skip if not in a git repo): - -```bash -git branch --show-current -``` - -```bash -git status --porcelain -``` - -```bash -git describe --tags --abbrev=0 -``` - -```bash -git rev-list <latest-tag>..HEAD --count -``` - -From the GitHub data fetched in Step 2, also check for the current branch's PR approval status: - -```bash -gh pr view --json number,title,url,reviewDecision,statusCheckRollup \ - --jq '{number,title,url,reviewDecision,checks: [.statusCheckRollup[]? | .status]}' -``` - -If `gh pr view` exits non-zero (no PR for current branch), note that there is no open PR. - -### 6b — Determine suggestion - -Evaluate the following conditions **in order**. Use the **first** match: - -| Priority | Condition | Output | -|----------|-----------|--------| -| 1 | On a `feature/*` branch with uncommitted changes (`git status --porcelain` is non-empty) | `What's next: You have uncommitted changes on \`<branch>\`. When ready, run \`/submit-for-review\`.` | -| 2 | On a `feature/*` branch with an open PR that has `reviewDecision: APPROVED` and all status checks are `COMPLETED` | `What's next: PR #<number> is approved and checks pass. Consider running \`/deploy\`.` | -| 3 | On a `feature/*` branch with an open PR (any other review/check state) | `What's next: PR #<number> (<title>) is open and awaiting review.` | -| 3.5 | Subject is `@me` and there are PRs requesting your review (from Step 2 query) | Append to the current suggestion (or show standalone if no higher priority matched): `You also have <N> PR(s) awaiting your review.` | -| 4 | On a `feature/*` branch with no open PR and clean working tree | `What's next: No open PR for \`<branch>\`. Run \`/submit-for-review\` to open one.` | -| 5 | On the integration branch (`dev`, `develop`, or `main` when no integration branch exists) with unreleased commits (rev-list count > 0 since last tag) | `What's next: <N> commit(s) on \`<branch>\` since \`<tag>\`. Run \`/deploy\` when ready to release.` | -| 6 | No open PRs, no open issues assigned to subject | `What's next: Nothing in progress. Run \`/start\` to begin new work.` | -| 7 | Open issues exist in "Up next" | `What's next: Next up is #<number> (<title>). Run \`/start <number>\` to pick it up.` | - -If none of the above match, omit the "What's next" section entirely. - -### 6c — Format - -Print the suggestion after a horizontal rule, below the local commits section: - -``` ---- -🧭 <suggestion text> -``` - -This section is omitted in milestone mode. - ---- - -## Milestone mode (Steps M1–M3) - -Only entered when `--milestone` or `--sprint` is detected in Step 1. - -### Step M1 — Fetch milestone issues +## Milestone mode (Steps M1–M2) +### M1 — Fetch ```bash gh issue list --milestone "<name>" --state all --limit 200 \ --json number,title,state,labels,assignees,url -``` - -If this command fails for any reason (milestone not found, auth error, etc.), report the error and stop. - -### Step M2 — Classify issues - -Fetch all open PRs to detect which issues are in progress (with health fields): - -```bash gh pr list --state open \ --json number,title,body,baseRefName,reviewDecision,statusCheckRollup,mergeable,isDraft ``` +If the issue query fails (milestone not found, auth error), report and stop. -Group issues into three buckets: - -- **Done** — `state: closed` -- **In progress** — `state: open` AND the issue number appears in any open PR body (look for `#<number>`, `closes #<number>`, `fixes #<number>`, `related to #<number>`, `issue #<number>`) -- **Not started** — `state: open` AND no open PR body references the issue number - -For in-progress issues, derive health badges from the linked PR using the same rules as Step 4a (check status, review decision, draft, conflict). - -### Step M3 — Output the summary - -``` -## Sprint: <name> - -<Y> of <total> issues closed · <Z> in progress · <W> not started - -### In progress (<Z>) -- #<number> <title> [@<assignee>] [<milestone>][ [draft]] - <url> · <check badge> · <review badge>[ · <conflict badge>] - -### Not started (<W>) -- #<number> <title> [@<assignee>] +### M2 — Classify and report -### Done (<Y>) -- #<number> <title> -``` - -Rules: -- Show "In progress" first, then "Not started", then "Done" -- Show assignee only if present; omit if unassigned -- Show URLs only for in-progress items; omit URLs for closed issues -- Show health badges on in-progress items (same derivation as Step 4a) -- If a section has no items, omit it entirely +Group the milestone's issues into three buckets: +- **Done** — `state: closed`. +- **In progress** — open, and the issue number is referenced by some open PR body (`#N`, `closes #N`, `fixes #N`, `related to #N`, `issue #N`). This "referenced by an open PR" definition is the real rule — apply it exactly. +- **Not started** — open, and no open PR references it. -Do not post, comment, write files, or take any action. Output only. +Derive health badges for in-progress issues from their linked PR (same fields as personal mode). Report as a scannable summary titled with the milestone name and a closed/in-progress/not-started roll-up. Show assignees and URLs where they add value; omit empty buckets. **Do not post, comment, write files, or take any action. Output only.** --- -## Team mode (Steps T1–T3) - -Only entered when `--team` is detected in Step 1. - -### Step T1 — Fetch all open work (run both in parallel) +## Team mode (Steps T1–T2) +### T1 — Fetch (run both concurrently) ```bash gh pr list --state open --limit 100 \ --json number,title,url,author,labels,milestone,baseRefName,body,reviewDecision,statusCheckRollup,updatedAt,mergeable,isDraft -``` - -```bash gh issue list --state open --limit 200 \ --json number,title,url,assignees,labels,milestone,updatedAt ``` +If either fails, report and stop. -If either command fails, report the error and stop. - -### Step T2 — Group and classify - -Group items by person: -- PRs are grouped by `author.login` -- Issues are grouped by assignee (first assignee if multiple). Issues with no assignee go into an "Unassigned" group. - -Within each person's group, classify items the same way as personal mode (Step 4): -- **In progress** — open PRs (and linked issues) -- **Up next** — open issues not linked from any open PR - -Derive health badges (Step 4a) and flag stale items (Step 4b) for all items. - -### Step T3 — Output the team summary - -``` -## Team status - -<N> open PRs · <N> open issues · <N> people - -### @<person> (<N> in progress, <N> up next) -- #<number> <title> — PR <check badge> · <review badge>[ · <conflict badge>][ · <stale badge>][ [draft]] - <url> -- #<number> <title> [up next][ · <stale badge>] - -### @<person> (<N> in progress, <N> up next) -... - -### Unassigned (<N>) -- #<number> <title> - <url> - -### ⚠️ Stale -- #<number> <title> (@<person>) — last updated <date> (<N> days ago) -``` - -Rules: -- Sort people alphabetically by username -- Within each person, show in-progress items first, then up-next items -- Show health badges on PR items (same format as personal mode) -- Show `[draft]` on draft PRs -- Tag up-next items with `[up next]` for visual distinction -- "Unassigned" section appears at the bottom, only if there are unassigned issues -- "Stale" section consolidates all stale items across all people -- Omit any section or group with no items -- No "What's next" section in team mode +### T2 — Group and report -Do not post, comment, write files, or take any action. Output only. +Group by person: PRs by `author.login`; issues by first assignee (unassigned issues into an "Unassigned" group). Within each person, classify as personal mode does — **in progress** (open PRs and their linked issues) and **up next** (open issues not referenced by any open PR) — and derive health badges plus staleness. Report per-person sections with in-progress items first, an "Unassigned" section if any, and a consolidated stale section. **Do not post, comment, write files, or take any action. Output only.** --- ## Hard rules -- Never write to GitHub (no comments, labels, issue updates, or PR changes). -- 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. +- Never write to GitHub (no comments, labels, issue updates, or PR changes) and never touch the working tree. Output only. +- If `gh` is unauthenticated or any fetch fails, report the error and stop immediately. Do not retry. +- Strip the leading `@` from the subject when passing to `gh` flags that reject it. +- `--team` is mutually exclusive with `--milestone`/`--sprint` and with a username. +- The `{{STALE_DAYS}}` threshold is config-driven; `0` disables staleness. The "what's next" priority ordering is fixed — evaluate top to bottom, first match wins. diff --git a/templates/codecannon.yaml b/templates/codecannon.yaml index eec4201..5c8c86c 100644 --- a/templates/codecannon.yaml +++ b/templates/codecannon.yaml @@ -55,6 +55,11 @@ config: DEV_CMD: make dev ABANDON_CMD: make abandon CHECK_CMD: make check + # Optional: command that runs the test suite during development, so /start's coding + # loop points at it instead of hand-rolling a test invocation. Leave empty (the default) + # if the project has no separate test target — /start degrades silently and verification + # rests on CHECK_CMD at the /submit-for-review gate. + # TEST_CMD: make test MERGE_CMD: make merge DEPLOY_PREVIEW_CMD: make deploy-preview DEPLOY_PROD_CMD: make deploy-prod diff --git a/tests/test_sync.py b/tests/test_sync.py index 8a3c585..099e3c3 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -400,6 +400,80 @@ def test_malformed_no_open_tag(self): self.assertIn("content", result) +# ═══════════════════════════════════════════════════════════════════════════════ +# DEPLOY SKILL — BRANCH-MODE RENDERING +# ═══════════════════════════════════════════════════════════════════════════════ + + +class TestDeployModeRendering(unittest.TestCase): + """Regression tests for #206: deploy.md's release steps were collapsed from three + near-identical per-mode copies into one shared path. These lock in that each of the + three branching modes still renders correctly — no leftover directives, the trunk + path has no promotion PR/merge, and the shared release mechanics appear exactly once. + """ + + MODES = { + "trunk": {"BRANCH_PROD": "main", "BRANCH_DEV": "", "BRANCH_TEST": ""}, + "two-branch": {"BRANCH_PROD": "main", "BRANCH_DEV": "dev", "BRANCH_TEST": ""}, + "three-branch": {"BRANCH_PROD": "main", "BRANCH_DEV": "dev", "BRANCH_TEST": "test"}, + } + + @classmethod + def setUpClass(cls): + text = (REPO_ROOT / "skills" / "github-agile" / "deploy.md").read_text() + cls.body = text.split("---\n", 2)[2] + + def _render(self, mode): + return sync.apply_conditionals(self.body, self.MODES[mode]) + + def test_no_leftover_directives_in_any_mode(self): + for mode in self.MODES: + out = self._render(mode) + self.assertNotIn("{{#if", out, f"{mode} left an #if directive") + self.assertNotIn("{{/if}}", out, f"{mode} left a /if directive") + + def test_trunk_has_no_promotion_pr_or_merge(self): + out = self._render("trunk") + self.assertNotIn("gh pr create --base", out) + self.assertNotIn("gh pr merge", out) + + def test_multibranch_has_promotion_pr_and_merge(self): + for mode in ("two-branch", "three-branch"): + out = self._render(mode) + self.assertIn("gh pr create --base", out, f"{mode} missing release PR") + self.assertIn("gh pr merge", out, f"{mode} missing merge") + + def test_release_creation_is_present_in_every_mode(self): + # Every mode must still create the GitHub Release exactly once (as a command). + for mode in self.MODES: + out = self._render(mode) + self.assertIn("gh release create <version-tag>", out, f"{mode} missing release") + + def test_publish_confirmation_appears_exactly_once(self): + # The public-publish gate must survive the de-duplication and not be triplicated. + for mode in self.MODES: + out = self._render(mode) + self.assertEqual( + out.count("Publishing GitHub Release"), 1, + f"{mode} should confirm the publish exactly once") + + def test_critical_unqualified_ref_note_survives_in_multibranch(self): + # The platform-behaviour note (unqualified #N populates closingIssuesReferences) + # is load-bearing and belongs to the promotion path only. + self.assertEqual(self._render("trunk").count("Critical:"), 0) + for mode in ("two-branch", "three-branch"): + self.assertEqual( + self._render(mode).count("Critical:"), 1, + f"{mode} lost the unqualified-#N platform note") + + def test_step_numbering_matches_mode(self): + trunk_steps = re.findall(r"^## Step (\d+) ", self._render("trunk"), re.M) + multi_steps = re.findall(r"^## Step (\d+) ", self._render("two-branch"), re.M) + # Trunk skips the promotion step, so it has one fewer numbered step. + self.assertEqual(trunk_steps, ["1", "2", "3", "4", "5", "6", "7"]) + self.assertEqual(multi_steps, ["1", "2", "3", "4", "5", "6", "7", "8"]) + + # ═══════════════════════════════════════════════════════════════════════════════ # PLACEHOLDER SUBSTITUTION # ═══════════════════════════════════════════════════════════════════════════════ From d636fe027132124a45b9bfdd19ec160f94745558 Mon Sep 17 00:00:00 2001 From: Sebastien Taggart <sebastien.taggart@gmail.com> Date: Wed, 5 Aug 2026 14:05:16 -0400 Subject: [PATCH 4/9] Prune /setup and /submit-for-review; fix permission-audit cd false positive via validate_only split --- .agents/skills/setup/SKILL.md | 131 +++------------------- .agents/skills/submit-for-review/SKILL.md | 45 +------- .claude/commands/setup.md | 131 +++------------------- .claude/commands/submit-for-review.md | 45 +------- .cursor/rules/setup.mdc | 131 +++------------------- .cursor/rules/submit-for-review.mdc | 45 +------- .gemini/skills/setup/SKILL.md | 131 +++------------------- .gemini/skills/submit-for-review/SKILL.md | 45 +------- docs/index.md | 3 +- permissions.yaml | 10 +- skills/github-agile/setup.md | 129 +++------------------ skills/github-agile/submit-for-review.md | 43 +------ sync.py | 22 +++- tests/test_sync.py | 43 +++++++ 14 files changed, 177 insertions(+), 777 deletions(-) diff --git a/.agents/skills/setup/SKILL.md b/.agents/skills/setup/SKILL.md index fb2ab27..6e45964 100644 --- a/.agents/skills/setup/SKILL.md +++ b/.agents/skills/setup/SKILL.md @@ -237,30 +237,7 @@ git show-ref --quiet --verify refs/remotes/origin/<BRANCH_DEV value> git show-ref --quiet --verify refs/remotes/origin/<BRANCH_TEST value> ``` -Display: - -``` -Setup looks healthy. Profile: <inferred profile> - - BRANCH_PROD: <value> - BRANCH_DEV: <value> (exists in remote: yes/no/not set) - BRANCH_TEST: <value> (exists in remote: yes/no/not set) - REVIEW_GATE: <value> - CHECK_CMD: <value> - MERGE_CMD: <value> - Adapters: <list from config> - - Optional config: - DEFAULT_MILESTONE — set / unset - DEFAULT_REVIEWERS — set / unset - TICKET_LABELS — set (N labels) / unset - TICKET_LABEL_CREATION_ALLOWED — set / unset - QA_READY_LABEL — set / unset - PLATFORM_COMPLIANCE_NOTES — set / unset - CONVENTIONS_NOTES — set / unset - SENSITIVE_AREAS_GATE — "true" (default) / "false" - SENSITIVE_AREAS_CATEGORIES — set (custom list) / unset (default 5-category list) -``` +Confirm the setup is healthy and show a scannable summary — lay it out however reads clearly. Include the inferred profile, the core workflow values (`BRANCH_PROD`; `BRANCH_DEV` and `BRANCH_TEST`, each with whether it exists in the remote; `REVIEW_GATE`; `CHECK_CMD`; `MERGE_CMD`; and the configured adapters), and then each optional config value reported as set or unset: `DEFAULT_MILESTONE`, `DEFAULT_REVIEWERS`, `TICKET_LABELS` (with label count when set), `TICKET_LABEL_CREATION_ALLOWED`, the QA labels, `PLATFORM_COMPLIANCE_NOTES`, `CONVENTIONS_NOTES`, `SENSITIVE_AREAS_GATE` (`"true"` default / `"false"`), and `SENSITIVE_AREAS_CATEGORIES` (custom list / default 5-category list). A value counts as "set" if it is present, uncommented, and non-empty in `.codecannon.yaml`. @@ -268,29 +245,13 @@ A value counts as "set" if it is present, uncommented, and non-empty in `.codeca ### Phase 2 — Permission audit -Check whether the agent's permission configuration covers the shell commands Code Cannon skills use. Read `CodeCannon/permissions.yaml` to get the list of required command prefixes. +Check whether the agent's permission configuration covers the shell commands Code Cannon skills use. Read the `commands:` list in `CodeCannon/permissions.yaml` for the required command prefixes. Use **only** the `commands:` key — commands under `validate_only:` (e.g. `cd`) are deliberately never emitted as allow rules, so they must not be reported as missing. -**Claude Code:** Read `.claude/settings.local.json` (if it exists) and `.claude/settings.json` (if it exists). Collect all `Bash(...)` entries from the `permissions.allow` arrays in both files. For each command prefix in `permissions.yaml`, check whether an allow rule covers it (e.g. `Bash(git:*)` or `Bash(git *)` covers the `git` prefix). +**Claude Code:** Read `.claude/settings.local.json` (if it exists) and `.claude/settings.json` (if it exists). Collect all `Bash(...)` entries from the `permissions.allow` arrays in both files. For each prefix in `commands:`, check whether an allow rule covers it (e.g. `Bash(git:*)` or `Bash(git *)` covers `git`). If all prefixes are covered, display `Agent permissions: all skill commands pre-approved` and continue to Phase 3. -If any prefixes are missing, show: - -``` -Agent permissions: some skill commands may prompt for approval. - - Missing allow rules: - - Bash(cd:*) - - Bash(make:*) - ... - - To pre-approve these, add them to .claude/settings.local.json (git-ignored) - or .claude/settings.json (shared with team). See docs/index.md for a full example. - - This is optional — you can approve commands individually when prompted instead. -``` - -Do not modify any settings file. This is advisory only. +If any are missing, report them as `Bash(<cmd>:*)` allow rules the user can optionally add to `.claude/settings.local.json` (git-ignored) or `.claude/settings.json` (shared with team) — pointing at `docs/index.md` for a full example — and note that commands can also be approved individually when prompted. Do not modify any settings file. This is advisory only. **Other agents (Cursor, Codex, Gemini):** Skip this phase silently — Cursor doesn't prompt, and Codex/Gemini permission systems vary. The docs cover these agents separately. @@ -319,22 +280,7 @@ Wait for response. git config --get user.signingkey ``` -**If a signing key is found**, show the proposed change and confirm: - -``` -I'll enable commit and tag signing for this repo: - - git config commit.gpgsign true - git config tag.gpgsign true - - Signing key: <truncated-key> - -Proceed? (yes/no) -``` - -Wait for confirmation. Write only on yes. If no, skip to Phase 4. - -Continue to Phase 4. +**If a signing key is found**, show the proposed change — enabling `commit.gpgsign` and `tag.gpgsign` for this repo, and naming the signing key — then ask "Proceed? (yes/no)". Wait for confirmation. Write only on yes. If no, skip to Phase 4. Otherwise continue to Phase 4. **If no signing key is found**, detect the signing format: @@ -345,19 +291,7 @@ git config --get gpg.format - If `ssh` → suggest: `git config user.signingkey ~/.ssh/id_ed25519.pub` (adjust path to the user's key). Ask the user for their SSH public key path. - If `gpg` or unset → suggest: run `gpg --list-secret-keys --keyid-format=long` to find a key ID. Ask the user for their GPG key ID. -Once the user provides a key value, show the proposed changes and confirm: - -``` -I'll configure signing for this repo: - - git config user.signingkey <provided-key> - git config commit.gpgsign true - git config tag.gpgsign true - -Proceed? (yes/no) -``` - -Wait for confirmation. Write only on yes. If no, skip to Phase 4. +Once the user provides a key value, show the proposed changes — setting `user.signingkey` to the provided key and enabling `commit.gpgsign` and `tag.gpgsign` — then ask "Proceed? (yes/no)". Wait for confirmation. Write only on yes. If no, skip to Phase 4. If the user has no signing key and doesn't know how to create one, point them to GitHub's signing key documentation and stop: "Set up a signing key first, then run `/setup` again to enable commit signing." @@ -371,26 +305,13 @@ Run: gh label list --limit 100 --json name,color,description ``` -If zero labels are found, treat this as a greenfield repository and offer a starter label baseline before asking about `TICKET_LABELS`. - -Show this recommendation: - -``` -No labels were found. For new projects, a practical baseline is: - - bug - - enhancement - - chore - - documentation - - ready-for-qa - - qa-passed - - qa-failed -``` - -Ask: **"Create any missing labels from this baseline now? (yes/no)"** - -Wait for response. +If zero labels are found, treat this as a greenfield repository. Present the starter baseline — `bug`, `enhancement`, `chore`, `documentation`, `ready-for-qa`, `qa-passed`, `qa-failed` — and ask: **"Create any missing labels from this baseline now? (yes/no)"** -- **yes** → run `python3 CodeCannon/skills/github-agile/scripts/label-create.py bug enhancement chore documentation ready-for-qa qa-passed qa-failed`. The script applies sensible color/description defaults from a baked-in table and warns-and-continues on any name that already exists. +- **yes** → run the label-create script with exactly those seven names: + ```bash + python3 CodeCannon/skills/github-agile/scripts/label-create.py bug enhancement chore documentation ready-for-qa qa-passed qa-failed + ``` + The script applies sensible color/description defaults from a baked-in table and warns-and-continues on any name that already exists. - **no / skip / anything else** → continue without creating labels. #### Configured-label audit @@ -408,21 +329,11 @@ The script reads `.codecannon.yaml`, collects the names referenced by `TICKET_LA - **yes** → run `python3 CodeCannon/skills/github-agile/scripts/label-create.py <name1> <name2> ...` with the missing names from the audit output. - **no / skip** → continue, but at the end of Phase 4 print a one-line summary: "Skipped creating: \<list\>. `/submit-for-review` will warn and continue if it needs to apply a missing label; `/qa` and `/start` may degrade similarly." -After this step (or if labels were non-zero initially), run `gh label list --limit 100 --json name,color,description` again. +After this step (or if labels were non-zero initially), re-run the same `gh label list` fetch to pick up any labels just created. If `TICKET_LABELS` is unset or fewer than 5 labels exist, add a note: "`/start` works best with a clear issue-label pool (`TICKET_LABELS`), and `/qa` needs explicit QA lifecycle labels (`ready-for-qa`, `qa-passed`, `qa-failed`). Consider a lightweight priority scheme (e.g. `priority:high`, `priority:medium`, `priority:low`) if the team needs triage support. If the team runs planned iterations, set `DEFAULT_MILESTONE` in Phase 5; otherwise leave it unset so `/start` auto-detects." -Display the results as a numbered list: - -``` -Available labels (N found): - 1. bug — Something isn't working - 2. enhancement — New feature or request - 3. good first issue — Good for newcomers - ... -``` - -Ask: **"Write these label names to `.codecannon.yaml` as TICKET_LABELS? (yes / no / list specific numbers)"** +Display the available labels as a numbered list (name — description, with the count found) so the user can pick by number, then ask: **"Write these label names to `.codecannon.yaml` as TICKET_LABELS? (yes / no / list specific numbers)"** Wait for the user's response. @@ -430,17 +341,7 @@ Wait for the user's response. - **numbers** (e.g. `1,3,5`) → use only those labels - **no / skip / anything else** → skip this phase, continue to Phase 5 -Show the exact change before writing: - -``` -I'll update .codecannon.yaml with: - - TICKET_LABELS: "bug,enhancement,..." - -Proceed? (yes/no) -``` - -Wait for confirmation. Write only on yes. +Show the exact change before writing — the `TICKET_LABELS` line as it will appear in `.codecannon.yaml` — and ask "Proceed? (yes/no)". Wait for confirmation. Write only on yes. --- @@ -491,4 +392,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 3, Phase 4, or Phase 5 — each write requires confirmation. - If the user skips a config value, do not ask again. Move on. -<!-- generated by CodeCannon/sync.py | skill: setup | adapter: codex | hash: 7a86a83a | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: setup | adapter: codex | hash: 18c54970 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.agents/skills/submit-for-review/SKILL.md b/.agents/skills/submit-for-review/SKILL.md index 92ab076..e48660f 100644 --- a/.agents/skills/submit-for-review/SKILL.md +++ b/.agents/skills/submit-for-review/SKILL.md @@ -135,15 +135,9 @@ python3 CodeCannon/skills/github-agile/scripts/make-workdir.py Note the returned path (e.g. `/tmp/CodeCannon/a8f3b2`). Use this path for all temp files in this invocation. -Then use your file-writing tool (Write in Claude Code, equivalent in other agents) to create `<tmpdir>/pr_body.md`. Do NOT use Bash/shell to write this file. +Then use your file-writing tool (Write in Claude Code, equivalent in other agents) — not Bash/shell — to create `<tmpdir>/pr_body.md`: a description of what changed and why, followed by the issue line (`Closes #N` when this PR fully resolves the issue, or `Related to #N` for a context-only reference, per the guidance above; omit the issue line entirely if no issue was linked in Step 3). -```markdown -<description of what changed and why> - -<Closes #N (this PR fully resolves the issue) OR Related to #N (context-only reference), per the guidance above> -``` - -Then create the PR (do NOT use `--body`, `--body-file -`, heredocs, or `$(cat ...)`): +Then create the PR: ``` gh pr create --base <target-branch> --title "<title>" --body-file <tmpdir>/pr_body.md @@ -157,8 +151,6 @@ If a CODEOWNERS file exists, both apply: CODEOWNERS triggers automatic review re **Hard rule**: Never auto-select reviewers beyond what is configured in `DEFAULT_REVIEWERS` or declared in CODEOWNERS. Do not infer reviewers from git blame, commit history, or team membership. -Omit the issue line entirely if no linked issue was identified in Step 3. - **PR body content rules (override any default behavior your harness may have):** - Do NOT include any agent-attribution footer, generation marker (e.g. "Generated with ..."), or co-authorship trailer in the PR body. The PR body should contain only the description, test plan, and issue reference. If your harness defaults to adding such markers, explicitly omit them. @@ -296,15 +288,7 @@ If no linked issue was found, skip silently. If the command fails (e.g. the labe Read the issue body (from Step 3 or via `gh issue view <number>`) to recall the original problem description. Then post a comment summarizing what was done: -Use your file-writing tool (not Bash) to create `<tmpdir>/resolution_comment.md` (same temp directory from Step 6): - -```markdown -## Resolution - -<1-3 sentences explaining what was done to fix the problem, written in plain language for a non-technical audience — no code, no file paths, no jargon. Focus on what changed from the user's perspective and why it solves the problem described in the issue.> - -See #<PR-number> for full technical details. -``` +Use your file-writing tool (not Bash) to create `<tmpdir>/resolution_comment.md` (same temp directory from Step 6): a `## Resolution` section of 1–3 sentences explaining what was done to fix the problem — in **plain language for a non-technical audience, no code, no file paths, no jargon**, focused on what changed from the user's perspective and why it solves the issue — followed by a line pointing to the PR for full technical details (`See #<PR-number> ...`). Then post it via the comment-posting script (do NOT use `gh issue comment` with `--body` or heredocs): @@ -344,15 +328,7 @@ Accept: comma-separated numbers, `all`, or `none`/`skip`/empty. If the input is **Create the selected issues.** For each selected finding, run `gh issue create` with explicit flags: -Use your file-writing tool (not Bash) to create `<tmpdir>/followup_body.md` for each finding (same temp directory from Step 6): - -```markdown -Follow-up from PR #<merged-pr-number> — auto-proposed from the code review. - -**Finding:** <full finding text, prefix included> - -See the review comment on the PR for context. -``` +Use your file-writing tool (not Bash) to create `<tmpdir>/followup_body.md` for each finding (same temp directory from Step 6): note it is a follow-up auto-proposed from the code review on PR #<merged-pr-number>, include the full finding text (prefix included), and point back to the review comment on the PR for context. Then create the issue (do NOT use `--body` or heredocs): @@ -377,16 +353,7 @@ If a single `gh issue create` call fails, report the failure for that finding an **Post a cross-link comment on the originating issue.** If one or more follow-ups were created **and** a linked originating issue number was identified in Step 3, post a single comment on that issue listing the new follow-ups so a reader of the thread can see the trailing work without digging into the PR. Skip silently if no follow-ups were created or no originating issue is linked. -Use your file-writing tool (not Bash) to create `<tmpdir>/followup_link_comment.md` (same temp directory from Step 6): - -```markdown -## Follow-up tickets from PR #<merged-pr-number> - -The code review on the PR for this issue surfaced non-blocking items tracked separately: - -- #<f1> — <title1> -- #<f2> — <title2> -``` +Use your file-writing tool (not Bash) to create `<tmpdir>/followup_link_comment.md` (same temp directory from Step 6): a short section headed for the follow-ups from PR #<merged-pr-number>, noting the review surfaced non-blocking items now tracked separately, then a bullet list of the new follow-up issues (`#<n> — <title>`). Then post via the comment-posting script (do NOT use `gh issue comment` with `--body` or heredocs): @@ -407,4 +374,4 @@ Use the unqualified `#N` form for all issue and PR references in the body. If `/ - `/submit-for-review` merges only to `dev` — never directly to `main`. - If `make merge` fails for any reason, report it and stop — do not attempt workarounds. - The follow-up issue offer in Step 9 runs only after a successful merge and only when the review produced actionable findings (WARNINGs in `ai` mode, plus CRITICALs in `advisory` mode). Never prompt the user for follow-ups when the review blocked the merge — those findings should be fixed, not ticketed. NOTEs never become follow-up tickets. -<!-- generated by CodeCannon/sync.py | skill: submit-for-review | adapter: codex | hash: 621f2b36 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: submit-for-review | adapter: codex | hash: 7a5f7fcd | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.claude/commands/setup.md b/.claude/commands/setup.md index 741750e..ec3d9e0 100644 --- a/.claude/commands/setup.md +++ b/.claude/commands/setup.md @@ -232,30 +232,7 @@ git show-ref --quiet --verify refs/remotes/origin/<BRANCH_DEV value> git show-ref --quiet --verify refs/remotes/origin/<BRANCH_TEST value> ``` -Display: - -``` -Setup looks healthy. Profile: <inferred profile> - - BRANCH_PROD: <value> - BRANCH_DEV: <value> (exists in remote: yes/no/not set) - BRANCH_TEST: <value> (exists in remote: yes/no/not set) - REVIEW_GATE: <value> - CHECK_CMD: <value> - MERGE_CMD: <value> - Adapters: <list from config> - - Optional config: - DEFAULT_MILESTONE — set / unset - DEFAULT_REVIEWERS — set / unset - TICKET_LABELS — set (N labels) / unset - TICKET_LABEL_CREATION_ALLOWED — set / unset - QA_READY_LABEL — set / unset - PLATFORM_COMPLIANCE_NOTES — set / unset - CONVENTIONS_NOTES — set / unset - SENSITIVE_AREAS_GATE — "true" (default) / "false" - SENSITIVE_AREAS_CATEGORIES — set (custom list) / unset (default 5-category list) -``` +Confirm the setup is healthy and show a scannable summary — lay it out however reads clearly. Include the inferred profile, the core workflow values (`BRANCH_PROD`; `BRANCH_DEV` and `BRANCH_TEST`, each with whether it exists in the remote; `REVIEW_GATE`; `CHECK_CMD`; `MERGE_CMD`; and the configured adapters), and then each optional config value reported as set or unset: `DEFAULT_MILESTONE`, `DEFAULT_REVIEWERS`, `TICKET_LABELS` (with label count when set), `TICKET_LABEL_CREATION_ALLOWED`, the QA labels, `PLATFORM_COMPLIANCE_NOTES`, `CONVENTIONS_NOTES`, `SENSITIVE_AREAS_GATE` (`"true"` default / `"false"`), and `SENSITIVE_AREAS_CATEGORIES` (custom list / default 5-category list). A value counts as "set" if it is present, uncommented, and non-empty in `.codecannon.yaml`. @@ -263,29 +240,13 @@ A value counts as "set" if it is present, uncommented, and non-empty in `.codeca ### Phase 2 — Permission audit -Check whether the agent's permission configuration covers the shell commands Code Cannon skills use. Read `CodeCannon/permissions.yaml` to get the list of required command prefixes. +Check whether the agent's permission configuration covers the shell commands Code Cannon skills use. Read the `commands:` list in `CodeCannon/permissions.yaml` for the required command prefixes. Use **only** the `commands:` key — commands under `validate_only:` (e.g. `cd`) are deliberately never emitted as allow rules, so they must not be reported as missing. -**Claude Code:** Read `.claude/settings.local.json` (if it exists) and `.claude/settings.json` (if it exists). Collect all `Bash(...)` entries from the `permissions.allow` arrays in both files. For each command prefix in `permissions.yaml`, check whether an allow rule covers it (e.g. `Bash(git:*)` or `Bash(git *)` covers the `git` prefix). +**Claude Code:** Read `.claude/settings.local.json` (if it exists) and `.claude/settings.json` (if it exists). Collect all `Bash(...)` entries from the `permissions.allow` arrays in both files. For each prefix in `commands:`, check whether an allow rule covers it (e.g. `Bash(git:*)` or `Bash(git *)` covers `git`). If all prefixes are covered, display `Agent permissions: all skill commands pre-approved` and continue to Phase 3. -If any prefixes are missing, show: - -``` -Agent permissions: some skill commands may prompt for approval. - - Missing allow rules: - - Bash(cd:*) - - Bash(make:*) - ... - - To pre-approve these, add them to .claude/settings.local.json (git-ignored) - or .claude/settings.json (shared with team). See docs/index.md for a full example. - - This is optional — you can approve commands individually when prompted instead. -``` - -Do not modify any settings file. This is advisory only. +If any are missing, report them as `Bash(<cmd>:*)` allow rules the user can optionally add to `.claude/settings.local.json` (git-ignored) or `.claude/settings.json` (shared with team) — pointing at `docs/index.md` for a full example — and note that commands can also be approved individually when prompted. Do not modify any settings file. This is advisory only. **Other agents (Cursor, Codex, Gemini):** Skip this phase silently — Cursor doesn't prompt, and Codex/Gemini permission systems vary. The docs cover these agents separately. @@ -314,22 +275,7 @@ Wait for response. git config --get user.signingkey ``` -**If a signing key is found**, show the proposed change and confirm: - -``` -I'll enable commit and tag signing for this repo: - - git config commit.gpgsign true - git config tag.gpgsign true - - Signing key: <truncated-key> - -Proceed? (yes/no) -``` - -Wait for confirmation. Write only on yes. If no, skip to Phase 4. - -Continue to Phase 4. +**If a signing key is found**, show the proposed change — enabling `commit.gpgsign` and `tag.gpgsign` for this repo, and naming the signing key — then ask "Proceed? (yes/no)". Wait for confirmation. Write only on yes. If no, skip to Phase 4. Otherwise continue to Phase 4. **If no signing key is found**, detect the signing format: @@ -340,19 +286,7 @@ git config --get gpg.format - If `ssh` → suggest: `git config user.signingkey ~/.ssh/id_ed25519.pub` (adjust path to the user's key). Ask the user for their SSH public key path. - If `gpg` or unset → suggest: run `gpg --list-secret-keys --keyid-format=long` to find a key ID. Ask the user for their GPG key ID. -Once the user provides a key value, show the proposed changes and confirm: - -``` -I'll configure signing for this repo: - - git config user.signingkey <provided-key> - git config commit.gpgsign true - git config tag.gpgsign true - -Proceed? (yes/no) -``` - -Wait for confirmation. Write only on yes. If no, skip to Phase 4. +Once the user provides a key value, show the proposed changes — setting `user.signingkey` to the provided key and enabling `commit.gpgsign` and `tag.gpgsign` — then ask "Proceed? (yes/no)". Wait for confirmation. Write only on yes. If no, skip to Phase 4. If the user has no signing key and doesn't know how to create one, point them to GitHub's signing key documentation and stop: "Set up a signing key first, then run `/setup` again to enable commit signing." @@ -366,26 +300,13 @@ Run: gh label list --limit 100 --json name,color,description ``` -If zero labels are found, treat this as a greenfield repository and offer a starter label baseline before asking about `TICKET_LABELS`. - -Show this recommendation: - -``` -No labels were found. For new projects, a practical baseline is: - - bug - - enhancement - - chore - - documentation - - ready-for-qa - - qa-passed - - qa-failed -``` - -Ask: **"Create any missing labels from this baseline now? (yes/no)"** - -Wait for response. +If zero labels are found, treat this as a greenfield repository. Present the starter baseline — `bug`, `enhancement`, `chore`, `documentation`, `ready-for-qa`, `qa-passed`, `qa-failed` — and ask: **"Create any missing labels from this baseline now? (yes/no)"** -- **yes** → run `python3 CodeCannon/skills/github-agile/scripts/label-create.py bug enhancement chore documentation ready-for-qa qa-passed qa-failed`. The script applies sensible color/description defaults from a baked-in table and warns-and-continues on any name that already exists. +- **yes** → run the label-create script with exactly those seven names: + ```bash + python3 CodeCannon/skills/github-agile/scripts/label-create.py bug enhancement chore documentation ready-for-qa qa-passed qa-failed + ``` + The script applies sensible color/description defaults from a baked-in table and warns-and-continues on any name that already exists. - **no / skip / anything else** → continue without creating labels. #### Configured-label audit @@ -403,21 +324,11 @@ The script reads `.codecannon.yaml`, collects the names referenced by `TICKET_LA - **yes** → run `python3 CodeCannon/skills/github-agile/scripts/label-create.py <name1> <name2> ...` with the missing names from the audit output. - **no / skip** → continue, but at the end of Phase 4 print a one-line summary: "Skipped creating: \<list\>. `/submit-for-review` will warn and continue if it needs to apply a missing label; `/qa` and `/start` may degrade similarly." -After this step (or if labels were non-zero initially), run `gh label list --limit 100 --json name,color,description` again. +After this step (or if labels were non-zero initially), re-run the same `gh label list` fetch to pick up any labels just created. If `TICKET_LABELS` is unset or fewer than 5 labels exist, add a note: "`/start` works best with a clear issue-label pool (`TICKET_LABELS`), and `/qa` needs explicit QA lifecycle labels (`ready-for-qa`, `qa-passed`, `qa-failed`). Consider a lightweight priority scheme (e.g. `priority:high`, `priority:medium`, `priority:low`) if the team needs triage support. If the team runs planned iterations, set `DEFAULT_MILESTONE` in Phase 5; otherwise leave it unset so `/start` auto-detects." -Display the results as a numbered list: - -``` -Available labels (N found): - 1. bug — Something isn't working - 2. enhancement — New feature or request - 3. good first issue — Good for newcomers - ... -``` - -Ask: **"Write these label names to `.codecannon.yaml` as TICKET_LABELS? (yes / no / list specific numbers)"** +Display the available labels as a numbered list (name — description, with the count found) so the user can pick by number, then ask: **"Write these label names to `.codecannon.yaml` as TICKET_LABELS? (yes / no / list specific numbers)"** Wait for the user's response. @@ -425,17 +336,7 @@ Wait for the user's response. - **numbers** (e.g. `1,3,5`) → use only those labels - **no / skip / anything else** → skip this phase, continue to Phase 5 -Show the exact change before writing: - -``` -I'll update .codecannon.yaml with: - - TICKET_LABELS: "bug,enhancement,..." - -Proceed? (yes/no) -``` - -Wait for confirmation. Write only on yes. +Show the exact change before writing — the `TICKET_LABELS` line as it will appear in `.codecannon.yaml` — and ask "Proceed? (yes/no)". Wait for confirmation. Write only on yes. --- @@ -486,4 +387,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 3, Phase 4, or Phase 5 — each write requires confirmation. - If the user skips a config value, do not ask again. Move on. -<!-- generated by CodeCannon/sync.py | skill: setup | adapter: claude | hash: ab9bf4d2 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: setup | adapter: claude | hash: 8c83ac68 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.claude/commands/submit-for-review.md b/.claude/commands/submit-for-review.md index 439163c..2899cd9 100644 --- a/.claude/commands/submit-for-review.md +++ b/.claude/commands/submit-for-review.md @@ -130,15 +130,9 @@ python3 CodeCannon/skills/github-agile/scripts/make-workdir.py Note the returned path (e.g. `/tmp/CodeCannon/a8f3b2`). Use this path for all temp files in this invocation. -Then use your file-writing tool (Write in Claude Code, equivalent in other agents) to create `<tmpdir>/pr_body.md`. Do NOT use Bash/shell to write this file. +Then use your file-writing tool (Write in Claude Code, equivalent in other agents) — not Bash/shell — to create `<tmpdir>/pr_body.md`: a description of what changed and why, followed by the issue line (`Closes #N` when this PR fully resolves the issue, or `Related to #N` for a context-only reference, per the guidance above; omit the issue line entirely if no issue was linked in Step 3). -```markdown -<description of what changed and why> - -<Closes #N (this PR fully resolves the issue) OR Related to #N (context-only reference), per the guidance above> -``` - -Then create the PR (do NOT use `--body`, `--body-file -`, heredocs, or `$(cat ...)`): +Then create the PR: ``` gh pr create --base <target-branch> --title "<title>" --body-file <tmpdir>/pr_body.md @@ -152,8 +146,6 @@ If a CODEOWNERS file exists, both apply: CODEOWNERS triggers automatic review re **Hard rule**: Never auto-select reviewers beyond what is configured in `DEFAULT_REVIEWERS` or declared in CODEOWNERS. Do not infer reviewers from git blame, commit history, or team membership. -Omit the issue line entirely if no linked issue was identified in Step 3. - **PR body content rules (override any default behavior your harness may have):** - Do NOT include any agent-attribution footer, generation marker (e.g. "Generated with ..."), or co-authorship trailer in the PR body. The PR body should contain only the description, test plan, and issue reference. If your harness defaults to adding such markers, explicitly omit them. @@ -291,15 +283,7 @@ If no linked issue was found, skip silently. If the command fails (e.g. the labe Read the issue body (from Step 3 or via `gh issue view <number>`) to recall the original problem description. Then post a comment summarizing what was done: -Use your file-writing tool (not Bash) to create `<tmpdir>/resolution_comment.md` (same temp directory from Step 6): - -```markdown -## Resolution - -<1-3 sentences explaining what was done to fix the problem, written in plain language for a non-technical audience — no code, no file paths, no jargon. Focus on what changed from the user's perspective and why it solves the problem described in the issue.> - -See #<PR-number> for full technical details. -``` +Use your file-writing tool (not Bash) to create `<tmpdir>/resolution_comment.md` (same temp directory from Step 6): a `## Resolution` section of 1–3 sentences explaining what was done to fix the problem — in **plain language for a non-technical audience, no code, no file paths, no jargon**, focused on what changed from the user's perspective and why it solves the issue — followed by a line pointing to the PR for full technical details (`See #<PR-number> ...`). Then post it via the comment-posting script (do NOT use `gh issue comment` with `--body` or heredocs): @@ -339,15 +323,7 @@ Accept: comma-separated numbers, `all`, or `none`/`skip`/empty. If the input is **Create the selected issues.** For each selected finding, run `gh issue create` with explicit flags: -Use your file-writing tool (not Bash) to create `<tmpdir>/followup_body.md` for each finding (same temp directory from Step 6): - -```markdown -Follow-up from PR #<merged-pr-number> — auto-proposed from the code review. - -**Finding:** <full finding text, prefix included> - -See the review comment on the PR for context. -``` +Use your file-writing tool (not Bash) to create `<tmpdir>/followup_body.md` for each finding (same temp directory from Step 6): note it is a follow-up auto-proposed from the code review on PR #<merged-pr-number>, include the full finding text (prefix included), and point back to the review comment on the PR for context. Then create the issue (do NOT use `--body` or heredocs): @@ -372,16 +348,7 @@ If a single `gh issue create` call fails, report the failure for that finding an **Post a cross-link comment on the originating issue.** If one or more follow-ups were created **and** a linked originating issue number was identified in Step 3, post a single comment on that issue listing the new follow-ups so a reader of the thread can see the trailing work without digging into the PR. Skip silently if no follow-ups were created or no originating issue is linked. -Use your file-writing tool (not Bash) to create `<tmpdir>/followup_link_comment.md` (same temp directory from Step 6): - -```markdown -## Follow-up tickets from PR #<merged-pr-number> - -The code review on the PR for this issue surfaced non-blocking items tracked separately: - -- #<f1> — <title1> -- #<f2> — <title2> -``` +Use your file-writing tool (not Bash) to create `<tmpdir>/followup_link_comment.md` (same temp directory from Step 6): a short section headed for the follow-ups from PR #<merged-pr-number>, noting the review surfaced non-blocking items now tracked separately, then a bullet list of the new follow-up issues (`#<n> — <title>`). Then post via the comment-posting script (do NOT use `gh issue comment` with `--body` or heredocs): @@ -402,4 +369,4 @@ Use the unqualified `#N` form for all issue and PR references in the body. If `/ - `/submit-for-review` merges only to `dev` — never directly to `main`. - If `make merge` fails for any reason, report it and stop — do not attempt workarounds. - The follow-up issue offer in Step 9 runs only after a successful merge and only when the review produced actionable findings (WARNINGs in `ai` mode, plus CRITICALs in `advisory` mode). Never prompt the user for follow-ups when the review blocked the merge — those findings should be fixed, not ticketed. NOTEs never become follow-up tickets. -<!-- generated by CodeCannon/sync.py | skill: submit-for-review | adapter: claude | hash: be0b8ed1 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: submit-for-review | adapter: claude | hash: 757a0fb2 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.cursor/rules/setup.mdc b/.cursor/rules/setup.mdc index d6243d3..1e14f8f 100644 --- a/.cursor/rules/setup.mdc +++ b/.cursor/rules/setup.mdc @@ -238,30 +238,7 @@ git show-ref --quiet --verify refs/remotes/origin/<BRANCH_DEV value> git show-ref --quiet --verify refs/remotes/origin/<BRANCH_TEST value> ``` -Display: - -``` -Setup looks healthy. Profile: <inferred profile> - - BRANCH_PROD: <value> - BRANCH_DEV: <value> (exists in remote: yes/no/not set) - BRANCH_TEST: <value> (exists in remote: yes/no/not set) - REVIEW_GATE: <value> - CHECK_CMD: <value> - MERGE_CMD: <value> - Adapters: <list from config> - - Optional config: - DEFAULT_MILESTONE — set / unset - DEFAULT_REVIEWERS — set / unset - TICKET_LABELS — set (N labels) / unset - TICKET_LABEL_CREATION_ALLOWED — set / unset - QA_READY_LABEL — set / unset - PLATFORM_COMPLIANCE_NOTES — set / unset - CONVENTIONS_NOTES — set / unset - SENSITIVE_AREAS_GATE — "true" (default) / "false" - SENSITIVE_AREAS_CATEGORIES — set (custom list) / unset (default 5-category list) -``` +Confirm the setup is healthy and show a scannable summary — lay it out however reads clearly. Include the inferred profile, the core workflow values (`BRANCH_PROD`; `BRANCH_DEV` and `BRANCH_TEST`, each with whether it exists in the remote; `REVIEW_GATE`; `CHECK_CMD`; `MERGE_CMD`; and the configured adapters), and then each optional config value reported as set or unset: `DEFAULT_MILESTONE`, `DEFAULT_REVIEWERS`, `TICKET_LABELS` (with label count when set), `TICKET_LABEL_CREATION_ALLOWED`, the QA labels, `PLATFORM_COMPLIANCE_NOTES`, `CONVENTIONS_NOTES`, `SENSITIVE_AREAS_GATE` (`"true"` default / `"false"`), and `SENSITIVE_AREAS_CATEGORIES` (custom list / default 5-category list). A value counts as "set" if it is present, uncommented, and non-empty in `.codecannon.yaml`. @@ -269,29 +246,13 @@ A value counts as "set" if it is present, uncommented, and non-empty in `.codeca ### Phase 2 — Permission audit -Check whether the agent's permission configuration covers the shell commands Code Cannon skills use. Read `CodeCannon/permissions.yaml` to get the list of required command prefixes. +Check whether the agent's permission configuration covers the shell commands Code Cannon skills use. Read the `commands:` list in `CodeCannon/permissions.yaml` for the required command prefixes. Use **only** the `commands:` key — commands under `validate_only:` (e.g. `cd`) are deliberately never emitted as allow rules, so they must not be reported as missing. -**Claude Code:** Read `.claude/settings.local.json` (if it exists) and `.claude/settings.json` (if it exists). Collect all `Bash(...)` entries from the `permissions.allow` arrays in both files. For each command prefix in `permissions.yaml`, check whether an allow rule covers it (e.g. `Bash(git:*)` or `Bash(git *)` covers the `git` prefix). +**Claude Code:** Read `.claude/settings.local.json` (if it exists) and `.claude/settings.json` (if it exists). Collect all `Bash(...)` entries from the `permissions.allow` arrays in both files. For each prefix in `commands:`, check whether an allow rule covers it (e.g. `Bash(git:*)` or `Bash(git *)` covers `git`). If all prefixes are covered, display `Agent permissions: all skill commands pre-approved` and continue to Phase 3. -If any prefixes are missing, show: - -``` -Agent permissions: some skill commands may prompt for approval. - - Missing allow rules: - - Bash(cd:*) - - Bash(make:*) - ... - - To pre-approve these, add them to .claude/settings.local.json (git-ignored) - or .claude/settings.json (shared with team). See docs/index.md for a full example. - - This is optional — you can approve commands individually when prompted instead. -``` - -Do not modify any settings file. This is advisory only. +If any are missing, report them as `Bash(<cmd>:*)` allow rules the user can optionally add to `.claude/settings.local.json` (git-ignored) or `.claude/settings.json` (shared with team) — pointing at `docs/index.md` for a full example — and note that commands can also be approved individually when prompted. Do not modify any settings file. This is advisory only. **Other agents (Cursor, Codex, Gemini):** Skip this phase silently — Cursor doesn't prompt, and Codex/Gemini permission systems vary. The docs cover these agents separately. @@ -320,22 +281,7 @@ Wait for response. git config --get user.signingkey ``` -**If a signing key is found**, show the proposed change and confirm: - -``` -I'll enable commit and tag signing for this repo: - - git config commit.gpgsign true - git config tag.gpgsign true - - Signing key: <truncated-key> - -Proceed? (yes/no) -``` - -Wait for confirmation. Write only on yes. If no, skip to Phase 4. - -Continue to Phase 4. +**If a signing key is found**, show the proposed change — enabling `commit.gpgsign` and `tag.gpgsign` for this repo, and naming the signing key — then ask "Proceed? (yes/no)". Wait for confirmation. Write only on yes. If no, skip to Phase 4. Otherwise continue to Phase 4. **If no signing key is found**, detect the signing format: @@ -346,19 +292,7 @@ git config --get gpg.format - If `ssh` → suggest: `git config user.signingkey ~/.ssh/id_ed25519.pub` (adjust path to the user's key). Ask the user for their SSH public key path. - If `gpg` or unset → suggest: run `gpg --list-secret-keys --keyid-format=long` to find a key ID. Ask the user for their GPG key ID. -Once the user provides a key value, show the proposed changes and confirm: - -``` -I'll configure signing for this repo: - - git config user.signingkey <provided-key> - git config commit.gpgsign true - git config tag.gpgsign true - -Proceed? (yes/no) -``` - -Wait for confirmation. Write only on yes. If no, skip to Phase 4. +Once the user provides a key value, show the proposed changes — setting `user.signingkey` to the provided key and enabling `commit.gpgsign` and `tag.gpgsign` — then ask "Proceed? (yes/no)". Wait for confirmation. Write only on yes. If no, skip to Phase 4. If the user has no signing key and doesn't know how to create one, point them to GitHub's signing key documentation and stop: "Set up a signing key first, then run `/setup` again to enable commit signing." @@ -372,26 +306,13 @@ Run: gh label list --limit 100 --json name,color,description ``` -If zero labels are found, treat this as a greenfield repository and offer a starter label baseline before asking about `TICKET_LABELS`. - -Show this recommendation: - -``` -No labels were found. For new projects, a practical baseline is: - - bug - - enhancement - - chore - - documentation - - ready-for-qa - - qa-passed - - qa-failed -``` - -Ask: **"Create any missing labels from this baseline now? (yes/no)"** - -Wait for response. +If zero labels are found, treat this as a greenfield repository. Present the starter baseline — `bug`, `enhancement`, `chore`, `documentation`, `ready-for-qa`, `qa-passed`, `qa-failed` — and ask: **"Create any missing labels from this baseline now? (yes/no)"** -- **yes** → run `python3 CodeCannon/skills/github-agile/scripts/label-create.py bug enhancement chore documentation ready-for-qa qa-passed qa-failed`. The script applies sensible color/description defaults from a baked-in table and warns-and-continues on any name that already exists. +- **yes** → run the label-create script with exactly those seven names: + ```bash + python3 CodeCannon/skills/github-agile/scripts/label-create.py bug enhancement chore documentation ready-for-qa qa-passed qa-failed + ``` + The script applies sensible color/description defaults from a baked-in table and warns-and-continues on any name that already exists. - **no / skip / anything else** → continue without creating labels. #### Configured-label audit @@ -409,21 +330,11 @@ The script reads `.codecannon.yaml`, collects the names referenced by `TICKET_LA - **yes** → run `python3 CodeCannon/skills/github-agile/scripts/label-create.py <name1> <name2> ...` with the missing names from the audit output. - **no / skip** → continue, but at the end of Phase 4 print a one-line summary: "Skipped creating: \<list\>. `/submit-for-review` will warn and continue if it needs to apply a missing label; `/qa` and `/start` may degrade similarly." -After this step (or if labels were non-zero initially), run `gh label list --limit 100 --json name,color,description` again. +After this step (or if labels were non-zero initially), re-run the same `gh label list` fetch to pick up any labels just created. If `TICKET_LABELS` is unset or fewer than 5 labels exist, add a note: "`/start` works best with a clear issue-label pool (`TICKET_LABELS`), and `/qa` needs explicit QA lifecycle labels (`ready-for-qa`, `qa-passed`, `qa-failed`). Consider a lightweight priority scheme (e.g. `priority:high`, `priority:medium`, `priority:low`) if the team needs triage support. If the team runs planned iterations, set `DEFAULT_MILESTONE` in Phase 5; otherwise leave it unset so `/start` auto-detects." -Display the results as a numbered list: - -``` -Available labels (N found): - 1. bug — Something isn't working - 2. enhancement — New feature or request - 3. good first issue — Good for newcomers - ... -``` - -Ask: **"Write these label names to `.codecannon.yaml` as TICKET_LABELS? (yes / no / list specific numbers)"** +Display the available labels as a numbered list (name — description, with the count found) so the user can pick by number, then ask: **"Write these label names to `.codecannon.yaml` as TICKET_LABELS? (yes / no / list specific numbers)"** Wait for the user's response. @@ -431,17 +342,7 @@ Wait for the user's response. - **numbers** (e.g. `1,3,5`) → use only those labels - **no / skip / anything else** → skip this phase, continue to Phase 5 -Show the exact change before writing: - -``` -I'll update .codecannon.yaml with: - - TICKET_LABELS: "bug,enhancement,..." - -Proceed? (yes/no) -``` - -Wait for confirmation. Write only on yes. +Show the exact change before writing — the `TICKET_LABELS` line as it will appear in `.codecannon.yaml` — and ask "Proceed? (yes/no)". Wait for confirmation. Write only on yes. --- @@ -492,4 +393,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 3, Phase 4, or Phase 5 — each write requires confirmation. - If the user skips a config value, do not ask again. Move on. -<!-- generated by CodeCannon/sync.py | skill: setup | adapter: cursor | hash: 27124dd1 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: setup | adapter: cursor | hash: ed1168bc | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.cursor/rules/submit-for-review.mdc b/.cursor/rules/submit-for-review.mdc index 8d71838..69dc4b9 100644 --- a/.cursor/rules/submit-for-review.mdc +++ b/.cursor/rules/submit-for-review.mdc @@ -136,15 +136,9 @@ python3 CodeCannon/skills/github-agile/scripts/make-workdir.py Note the returned path (e.g. `/tmp/CodeCannon/a8f3b2`). Use this path for all temp files in this invocation. -Then use your file-writing tool (Write in Claude Code, equivalent in other agents) to create `<tmpdir>/pr_body.md`. Do NOT use Bash/shell to write this file. +Then use your file-writing tool (Write in Claude Code, equivalent in other agents) — not Bash/shell — to create `<tmpdir>/pr_body.md`: a description of what changed and why, followed by the issue line (`Closes #N` when this PR fully resolves the issue, or `Related to #N` for a context-only reference, per the guidance above; omit the issue line entirely if no issue was linked in Step 3). -```markdown -<description of what changed and why> - -<Closes #N (this PR fully resolves the issue) OR Related to #N (context-only reference), per the guidance above> -``` - -Then create the PR (do NOT use `--body`, `--body-file -`, heredocs, or `$(cat ...)`): +Then create the PR: ``` gh pr create --base <target-branch> --title "<title>" --body-file <tmpdir>/pr_body.md @@ -158,8 +152,6 @@ If a CODEOWNERS file exists, both apply: CODEOWNERS triggers automatic review re **Hard rule**: Never auto-select reviewers beyond what is configured in `DEFAULT_REVIEWERS` or declared in CODEOWNERS. Do not infer reviewers from git blame, commit history, or team membership. -Omit the issue line entirely if no linked issue was identified in Step 3. - **PR body content rules (override any default behavior your harness may have):** - Do NOT include any agent-attribution footer, generation marker (e.g. "Generated with ..."), or co-authorship trailer in the PR body. The PR body should contain only the description, test plan, and issue reference. If your harness defaults to adding such markers, explicitly omit them. @@ -297,15 +289,7 @@ If no linked issue was found, skip silently. If the command fails (e.g. the labe Read the issue body (from Step 3 or via `gh issue view <number>`) to recall the original problem description. Then post a comment summarizing what was done: -Use your file-writing tool (not Bash) to create `<tmpdir>/resolution_comment.md` (same temp directory from Step 6): - -```markdown -## Resolution - -<1-3 sentences explaining what was done to fix the problem, written in plain language for a non-technical audience — no code, no file paths, no jargon. Focus on what changed from the user's perspective and why it solves the problem described in the issue.> - -See #<PR-number> for full technical details. -``` +Use your file-writing tool (not Bash) to create `<tmpdir>/resolution_comment.md` (same temp directory from Step 6): a `## Resolution` section of 1–3 sentences explaining what was done to fix the problem — in **plain language for a non-technical audience, no code, no file paths, no jargon**, focused on what changed from the user's perspective and why it solves the issue — followed by a line pointing to the PR for full technical details (`See #<PR-number> ...`). Then post it via the comment-posting script (do NOT use `gh issue comment` with `--body` or heredocs): @@ -345,15 +329,7 @@ Accept: comma-separated numbers, `all`, or `none`/`skip`/empty. If the input is **Create the selected issues.** For each selected finding, run `gh issue create` with explicit flags: -Use your file-writing tool (not Bash) to create `<tmpdir>/followup_body.md` for each finding (same temp directory from Step 6): - -```markdown -Follow-up from PR #<merged-pr-number> — auto-proposed from the code review. - -**Finding:** <full finding text, prefix included> - -See the review comment on the PR for context. -``` +Use your file-writing tool (not Bash) to create `<tmpdir>/followup_body.md` for each finding (same temp directory from Step 6): note it is a follow-up auto-proposed from the code review on PR #<merged-pr-number>, include the full finding text (prefix included), and point back to the review comment on the PR for context. Then create the issue (do NOT use `--body` or heredocs): @@ -378,16 +354,7 @@ If a single `gh issue create` call fails, report the failure for that finding an **Post a cross-link comment on the originating issue.** If one or more follow-ups were created **and** a linked originating issue number was identified in Step 3, post a single comment on that issue listing the new follow-ups so a reader of the thread can see the trailing work without digging into the PR. Skip silently if no follow-ups were created or no originating issue is linked. -Use your file-writing tool (not Bash) to create `<tmpdir>/followup_link_comment.md` (same temp directory from Step 6): - -```markdown -## Follow-up tickets from PR #<merged-pr-number> - -The code review on the PR for this issue surfaced non-blocking items tracked separately: - -- #<f1> — <title1> -- #<f2> — <title2> -``` +Use your file-writing tool (not Bash) to create `<tmpdir>/followup_link_comment.md` (same temp directory from Step 6): a short section headed for the follow-ups from PR #<merged-pr-number>, noting the review surfaced non-blocking items now tracked separately, then a bullet list of the new follow-up issues (`#<n> — <title>`). Then post via the comment-posting script (do NOT use `gh issue comment` with `--body` or heredocs): @@ -408,4 +375,4 @@ Use the unqualified `#N` form for all issue and PR references in the body. If `/ - `/submit-for-review` merges only to `dev` — never directly to `main`. - If `make merge` fails for any reason, report it and stop — do not attempt workarounds. - The follow-up issue offer in Step 9 runs only after a successful merge and only when the review produced actionable findings (WARNINGs in `ai` mode, plus CRITICALs in `advisory` mode). Never prompt the user for follow-ups when the review blocked the merge — those findings should be fixed, not ticketed. NOTEs never become follow-up tickets. -<!-- generated by CodeCannon/sync.py | skill: submit-for-review | adapter: cursor | hash: 1aaa7089 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: submit-for-review | adapter: cursor | hash: d6de4406 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.gemini/skills/setup/SKILL.md b/.gemini/skills/setup/SKILL.md index 0fb8a5c..010ea67 100644 --- a/.gemini/skills/setup/SKILL.md +++ b/.gemini/skills/setup/SKILL.md @@ -237,30 +237,7 @@ git show-ref --quiet --verify refs/remotes/origin/<BRANCH_DEV value> git show-ref --quiet --verify refs/remotes/origin/<BRANCH_TEST value> ``` -Display: - -``` -Setup looks healthy. Profile: <inferred profile> - - BRANCH_PROD: <value> - BRANCH_DEV: <value> (exists in remote: yes/no/not set) - BRANCH_TEST: <value> (exists in remote: yes/no/not set) - REVIEW_GATE: <value> - CHECK_CMD: <value> - MERGE_CMD: <value> - Adapters: <list from config> - - Optional config: - DEFAULT_MILESTONE — set / unset - DEFAULT_REVIEWERS — set / unset - TICKET_LABELS — set (N labels) / unset - TICKET_LABEL_CREATION_ALLOWED — set / unset - QA_READY_LABEL — set / unset - PLATFORM_COMPLIANCE_NOTES — set / unset - CONVENTIONS_NOTES — set / unset - SENSITIVE_AREAS_GATE — "true" (default) / "false" - SENSITIVE_AREAS_CATEGORIES — set (custom list) / unset (default 5-category list) -``` +Confirm the setup is healthy and show a scannable summary — lay it out however reads clearly. Include the inferred profile, the core workflow values (`BRANCH_PROD`; `BRANCH_DEV` and `BRANCH_TEST`, each with whether it exists in the remote; `REVIEW_GATE`; `CHECK_CMD`; `MERGE_CMD`; and the configured adapters), and then each optional config value reported as set or unset: `DEFAULT_MILESTONE`, `DEFAULT_REVIEWERS`, `TICKET_LABELS` (with label count when set), `TICKET_LABEL_CREATION_ALLOWED`, the QA labels, `PLATFORM_COMPLIANCE_NOTES`, `CONVENTIONS_NOTES`, `SENSITIVE_AREAS_GATE` (`"true"` default / `"false"`), and `SENSITIVE_AREAS_CATEGORIES` (custom list / default 5-category list). A value counts as "set" if it is present, uncommented, and non-empty in `.codecannon.yaml`. @@ -268,29 +245,13 @@ A value counts as "set" if it is present, uncommented, and non-empty in `.codeca ### Phase 2 — Permission audit -Check whether the agent's permission configuration covers the shell commands Code Cannon skills use. Read `CodeCannon/permissions.yaml` to get the list of required command prefixes. +Check whether the agent's permission configuration covers the shell commands Code Cannon skills use. Read the `commands:` list in `CodeCannon/permissions.yaml` for the required command prefixes. Use **only** the `commands:` key — commands under `validate_only:` (e.g. `cd`) are deliberately never emitted as allow rules, so they must not be reported as missing. -**Claude Code:** Read `.claude/settings.local.json` (if it exists) and `.claude/settings.json` (if it exists). Collect all `Bash(...)` entries from the `permissions.allow` arrays in both files. For each command prefix in `permissions.yaml`, check whether an allow rule covers it (e.g. `Bash(git:*)` or `Bash(git *)` covers the `git` prefix). +**Claude Code:** Read `.claude/settings.local.json` (if it exists) and `.claude/settings.json` (if it exists). Collect all `Bash(...)` entries from the `permissions.allow` arrays in both files. For each prefix in `commands:`, check whether an allow rule covers it (e.g. `Bash(git:*)` or `Bash(git *)` covers `git`). If all prefixes are covered, display `Agent permissions: all skill commands pre-approved` and continue to Phase 3. -If any prefixes are missing, show: - -``` -Agent permissions: some skill commands may prompt for approval. - - Missing allow rules: - - Bash(cd:*) - - Bash(make:*) - ... - - To pre-approve these, add them to .claude/settings.local.json (git-ignored) - or .claude/settings.json (shared with team). See docs/index.md for a full example. - - This is optional — you can approve commands individually when prompted instead. -``` - -Do not modify any settings file. This is advisory only. +If any are missing, report them as `Bash(<cmd>:*)` allow rules the user can optionally add to `.claude/settings.local.json` (git-ignored) or `.claude/settings.json` (shared with team) — pointing at `docs/index.md` for a full example — and note that commands can also be approved individually when prompted. Do not modify any settings file. This is advisory only. **Other agents (Cursor, Codex, Gemini):** Skip this phase silently — Cursor doesn't prompt, and Codex/Gemini permission systems vary. The docs cover these agents separately. @@ -319,22 +280,7 @@ Wait for response. git config --get user.signingkey ``` -**If a signing key is found**, show the proposed change and confirm: - -``` -I'll enable commit and tag signing for this repo: - - git config commit.gpgsign true - git config tag.gpgsign true - - Signing key: <truncated-key> - -Proceed? (yes/no) -``` - -Wait for confirmation. Write only on yes. If no, skip to Phase 4. - -Continue to Phase 4. +**If a signing key is found**, show the proposed change — enabling `commit.gpgsign` and `tag.gpgsign` for this repo, and naming the signing key — then ask "Proceed? (yes/no)". Wait for confirmation. Write only on yes. If no, skip to Phase 4. Otherwise continue to Phase 4. **If no signing key is found**, detect the signing format: @@ -345,19 +291,7 @@ git config --get gpg.format - If `ssh` → suggest: `git config user.signingkey ~/.ssh/id_ed25519.pub` (adjust path to the user's key). Ask the user for their SSH public key path. - If `gpg` or unset → suggest: run `gpg --list-secret-keys --keyid-format=long` to find a key ID. Ask the user for their GPG key ID. -Once the user provides a key value, show the proposed changes and confirm: - -``` -I'll configure signing for this repo: - - git config user.signingkey <provided-key> - git config commit.gpgsign true - git config tag.gpgsign true - -Proceed? (yes/no) -``` - -Wait for confirmation. Write only on yes. If no, skip to Phase 4. +Once the user provides a key value, show the proposed changes — setting `user.signingkey` to the provided key and enabling `commit.gpgsign` and `tag.gpgsign` — then ask "Proceed? (yes/no)". Wait for confirmation. Write only on yes. If no, skip to Phase 4. If the user has no signing key and doesn't know how to create one, point them to GitHub's signing key documentation and stop: "Set up a signing key first, then run `/setup` again to enable commit signing." @@ -371,26 +305,13 @@ Run: gh label list --limit 100 --json name,color,description ``` -If zero labels are found, treat this as a greenfield repository and offer a starter label baseline before asking about `TICKET_LABELS`. - -Show this recommendation: - -``` -No labels were found. For new projects, a practical baseline is: - - bug - - enhancement - - chore - - documentation - - ready-for-qa - - qa-passed - - qa-failed -``` - -Ask: **"Create any missing labels from this baseline now? (yes/no)"** - -Wait for response. +If zero labels are found, treat this as a greenfield repository. Present the starter baseline — `bug`, `enhancement`, `chore`, `documentation`, `ready-for-qa`, `qa-passed`, `qa-failed` — and ask: **"Create any missing labels from this baseline now? (yes/no)"** -- **yes** → run `python3 CodeCannon/skills/github-agile/scripts/label-create.py bug enhancement chore documentation ready-for-qa qa-passed qa-failed`. The script applies sensible color/description defaults from a baked-in table and warns-and-continues on any name that already exists. +- **yes** → run the label-create script with exactly those seven names: + ```bash + python3 CodeCannon/skills/github-agile/scripts/label-create.py bug enhancement chore documentation ready-for-qa qa-passed qa-failed + ``` + The script applies sensible color/description defaults from a baked-in table and warns-and-continues on any name that already exists. - **no / skip / anything else** → continue without creating labels. #### Configured-label audit @@ -408,21 +329,11 @@ The script reads `.codecannon.yaml`, collects the names referenced by `TICKET_LA - **yes** → run `python3 CodeCannon/skills/github-agile/scripts/label-create.py <name1> <name2> ...` with the missing names from the audit output. - **no / skip** → continue, but at the end of Phase 4 print a one-line summary: "Skipped creating: \<list\>. `/submit-for-review` will warn and continue if it needs to apply a missing label; `/qa` and `/start` may degrade similarly." -After this step (or if labels were non-zero initially), run `gh label list --limit 100 --json name,color,description` again. +After this step (or if labels were non-zero initially), re-run the same `gh label list` fetch to pick up any labels just created. If `TICKET_LABELS` is unset or fewer than 5 labels exist, add a note: "`/start` works best with a clear issue-label pool (`TICKET_LABELS`), and `/qa` needs explicit QA lifecycle labels (`ready-for-qa`, `qa-passed`, `qa-failed`). Consider a lightweight priority scheme (e.g. `priority:high`, `priority:medium`, `priority:low`) if the team needs triage support. If the team runs planned iterations, set `DEFAULT_MILESTONE` in Phase 5; otherwise leave it unset so `/start` auto-detects." -Display the results as a numbered list: - -``` -Available labels (N found): - 1. bug — Something isn't working - 2. enhancement — New feature or request - 3. good first issue — Good for newcomers - ... -``` - -Ask: **"Write these label names to `.codecannon.yaml` as TICKET_LABELS? (yes / no / list specific numbers)"** +Display the available labels as a numbered list (name — description, with the count found) so the user can pick by number, then ask: **"Write these label names to `.codecannon.yaml` as TICKET_LABELS? (yes / no / list specific numbers)"** Wait for the user's response. @@ -430,17 +341,7 @@ Wait for the user's response. - **numbers** (e.g. `1,3,5`) → use only those labels - **no / skip / anything else** → skip this phase, continue to Phase 5 -Show the exact change before writing: - -``` -I'll update .codecannon.yaml with: - - TICKET_LABELS: "bug,enhancement,..." - -Proceed? (yes/no) -``` - -Wait for confirmation. Write only on yes. +Show the exact change before writing — the `TICKET_LABELS` line as it will appear in `.codecannon.yaml` — and ask "Proceed? (yes/no)". Wait for confirmation. Write only on yes. --- @@ -491,4 +392,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 3, Phase 4, or Phase 5 — each write requires confirmation. - If the user skips a config value, do not ask again. Move on. -<!-- generated by CodeCannon/sync.py | skill: setup | adapter: gemini | hash: 38f9e584 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: setup | adapter: gemini | hash: dc967ad7 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.gemini/skills/submit-for-review/SKILL.md b/.gemini/skills/submit-for-review/SKILL.md index 493e9df..3bc9f8e 100644 --- a/.gemini/skills/submit-for-review/SKILL.md +++ b/.gemini/skills/submit-for-review/SKILL.md @@ -135,15 +135,9 @@ python3 CodeCannon/skills/github-agile/scripts/make-workdir.py Note the returned path (e.g. `/tmp/CodeCannon/a8f3b2`). Use this path for all temp files in this invocation. -Then use your file-writing tool (Write in Claude Code, equivalent in other agents) to create `<tmpdir>/pr_body.md`. Do NOT use Bash/shell to write this file. +Then use your file-writing tool (Write in Claude Code, equivalent in other agents) — not Bash/shell — to create `<tmpdir>/pr_body.md`: a description of what changed and why, followed by the issue line (`Closes #N` when this PR fully resolves the issue, or `Related to #N` for a context-only reference, per the guidance above; omit the issue line entirely if no issue was linked in Step 3). -```markdown -<description of what changed and why> - -<Closes #N (this PR fully resolves the issue) OR Related to #N (context-only reference), per the guidance above> -``` - -Then create the PR (do NOT use `--body`, `--body-file -`, heredocs, or `$(cat ...)`): +Then create the PR: ``` gh pr create --base <target-branch> --title "<title>" --body-file <tmpdir>/pr_body.md @@ -157,8 +151,6 @@ If a CODEOWNERS file exists, both apply: CODEOWNERS triggers automatic review re **Hard rule**: Never auto-select reviewers beyond what is configured in `DEFAULT_REVIEWERS` or declared in CODEOWNERS. Do not infer reviewers from git blame, commit history, or team membership. -Omit the issue line entirely if no linked issue was identified in Step 3. - **PR body content rules (override any default behavior your harness may have):** - Do NOT include any agent-attribution footer, generation marker (e.g. "Generated with ..."), or co-authorship trailer in the PR body. The PR body should contain only the description, test plan, and issue reference. If your harness defaults to adding such markers, explicitly omit them. @@ -296,15 +288,7 @@ If no linked issue was found, skip silently. If the command fails (e.g. the labe Read the issue body (from Step 3 or via `gh issue view <number>`) to recall the original problem description. Then post a comment summarizing what was done: -Use your file-writing tool (not Bash) to create `<tmpdir>/resolution_comment.md` (same temp directory from Step 6): - -```markdown -## Resolution - -<1-3 sentences explaining what was done to fix the problem, written in plain language for a non-technical audience — no code, no file paths, no jargon. Focus on what changed from the user's perspective and why it solves the problem described in the issue.> - -See #<PR-number> for full technical details. -``` +Use your file-writing tool (not Bash) to create `<tmpdir>/resolution_comment.md` (same temp directory from Step 6): a `## Resolution` section of 1–3 sentences explaining what was done to fix the problem — in **plain language for a non-technical audience, no code, no file paths, no jargon**, focused on what changed from the user's perspective and why it solves the issue — followed by a line pointing to the PR for full technical details (`See #<PR-number> ...`). Then post it via the comment-posting script (do NOT use `gh issue comment` with `--body` or heredocs): @@ -344,15 +328,7 @@ Accept: comma-separated numbers, `all`, or `none`/`skip`/empty. If the input is **Create the selected issues.** For each selected finding, run `gh issue create` with explicit flags: -Use your file-writing tool (not Bash) to create `<tmpdir>/followup_body.md` for each finding (same temp directory from Step 6): - -```markdown -Follow-up from PR #<merged-pr-number> — auto-proposed from the code review. - -**Finding:** <full finding text, prefix included> - -See the review comment on the PR for context. -``` +Use your file-writing tool (not Bash) to create `<tmpdir>/followup_body.md` for each finding (same temp directory from Step 6): note it is a follow-up auto-proposed from the code review on PR #<merged-pr-number>, include the full finding text (prefix included), and point back to the review comment on the PR for context. Then create the issue (do NOT use `--body` or heredocs): @@ -377,16 +353,7 @@ If a single `gh issue create` call fails, report the failure for that finding an **Post a cross-link comment on the originating issue.** If one or more follow-ups were created **and** a linked originating issue number was identified in Step 3, post a single comment on that issue listing the new follow-ups so a reader of the thread can see the trailing work without digging into the PR. Skip silently if no follow-ups were created or no originating issue is linked. -Use your file-writing tool (not Bash) to create `<tmpdir>/followup_link_comment.md` (same temp directory from Step 6): - -```markdown -## Follow-up tickets from PR #<merged-pr-number> - -The code review on the PR for this issue surfaced non-blocking items tracked separately: - -- #<f1> — <title1> -- #<f2> — <title2> -``` +Use your file-writing tool (not Bash) to create `<tmpdir>/followup_link_comment.md` (same temp directory from Step 6): a short section headed for the follow-ups from PR #<merged-pr-number>, noting the review surfaced non-blocking items now tracked separately, then a bullet list of the new follow-up issues (`#<n> — <title>`). Then post via the comment-posting script (do NOT use `gh issue comment` with `--body` or heredocs): @@ -407,4 +374,4 @@ Use the unqualified `#N` form for all issue and PR references in the body. If `/ - `/submit-for-review` merges only to `dev` — never directly to `main`. - If `make merge` fails for any reason, report it and stop — do not attempt workarounds. - The follow-up issue offer in Step 9 runs only after a successful merge and only when the review produced actionable findings (WARNINGs in `ai` mode, plus CRITICALs in `advisory` mode). Never prompt the user for follow-ups when the review blocked the merge — those findings should be fixed, not ticketed. NOTEs never become follow-up tickets. -<!-- generated by CodeCannon/sync.py | skill: submit-for-review | adapter: gemini | hash: 4620184f | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: submit-for-review | adapter: gemini | hash: dd7b55b2 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/docs/index.md b/docs/index.md index f81e24b..2977948 100644 --- a/docs/index.md +++ b/docs/index.md @@ -123,7 +123,6 @@ Code Cannon skills are agent-agnostic, but each agent has its own quirks. This s "permissions": { "defaultMode": "acceptEdits", "allow": [ - "Bash(cd:*)", "Bash(git:*)", "Bash(gh:*)", "Bash(make:*)", @@ -144,7 +143,7 @@ Code Cannon skills are agent-agnostic, but each agent has its own quirks. This s } ``` -`defaultMode: "acceptEdits"` auto-approves file edits and common filesystem ops. `allow` rules pre-approve matching bash commands (wildcards supported). `deny` rules always win — dangerous operations still prompt. Adjust the `allow` list to match your project's tooling. +`defaultMode: "acceptEdits"` auto-approves file edits and common filesystem ops. `allow` rules pre-approve matching bash commands (wildcards supported). `deny` rules always win — dangerous operations still prompt. Adjust the `allow` list to match your project's tooling. (`Bash(cd:*)` is intentionally omitted — Code Cannon skills use single, statically-analyzable commands, and blessing `cd` would re-invite the compound `cd … && …` shape the allowlist is designed to avoid; `/setup`'s permission audit will not ask for it.) ### Cursor diff --git a/permissions.yaml b/permissions.yaml index ca0a0b0..e3ec8f4 100644 --- a/permissions.yaml +++ b/permissions.yaml @@ -15,8 +15,8 @@ # (no `&&`/`|`/`;` chains, no `$(...)`, no redirections) — sync.py --validate # enforces this. Push irreducibly-complex commands into scripts/ instead. +# Emitted as allow rules AND validated in skill code blocks. commands: - - cd - cp - gh - git @@ -28,3 +28,11 @@ commands: - python3 - test - which + +# Validated in skill code blocks but deliberately NOT emitted as allow rules. +# Blessing these would re-invite the compound shapes #202 removed (e.g. `cd … && …`). +# Keeping the exclusion here — rather than as a special case in code — means the +# emitter, the shape/prefix validators, and the /setup permission audit all agree +# without any of them hardcoding a command name. +validate_only: + - cd diff --git a/skills/github-agile/setup.md b/skills/github-agile/setup.md index 9bdf095..fa636db 100644 --- a/skills/github-agile/setup.md +++ b/skills/github-agile/setup.md @@ -235,30 +235,7 @@ git show-ref --quiet --verify refs/remotes/origin/<BRANCH_DEV value> git show-ref --quiet --verify refs/remotes/origin/<BRANCH_TEST value> ``` -Display: - -``` -Setup looks healthy. Profile: <inferred profile> - - BRANCH_PROD: <value> - BRANCH_DEV: <value> (exists in remote: yes/no/not set) - BRANCH_TEST: <value> (exists in remote: yes/no/not set) - REVIEW_GATE: <value> - CHECK_CMD: <value> - MERGE_CMD: <value> - Adapters: <list from config> - - Optional config: - DEFAULT_MILESTONE — set / unset - DEFAULT_REVIEWERS — set / unset - TICKET_LABELS — set (N labels) / unset - TICKET_LABEL_CREATION_ALLOWED — set / unset - QA_READY_LABEL — set / unset - PLATFORM_COMPLIANCE_NOTES — set / unset - CONVENTIONS_NOTES — set / unset - SENSITIVE_AREAS_GATE — "true" (default) / "false" - SENSITIVE_AREAS_CATEGORIES — set (custom list) / unset (default 5-category list) -``` +Confirm the setup is healthy and show a scannable summary — lay it out however reads clearly. Include the inferred profile, the core workflow values (`BRANCH_PROD`; `BRANCH_DEV` and `BRANCH_TEST`, each with whether it exists in the remote; `REVIEW_GATE`; `CHECK_CMD`; `MERGE_CMD`; and the configured adapters), and then each optional config value reported as set or unset: `DEFAULT_MILESTONE`, `DEFAULT_REVIEWERS`, `TICKET_LABELS` (with label count when set), `TICKET_LABEL_CREATION_ALLOWED`, the QA labels, `PLATFORM_COMPLIANCE_NOTES`, `CONVENTIONS_NOTES`, `SENSITIVE_AREAS_GATE` (`"true"` default / `"false"`), and `SENSITIVE_AREAS_CATEGORIES` (custom list / default 5-category list). A value counts as "set" if it is present, uncommented, and non-empty in `.codecannon.yaml`. @@ -266,29 +243,13 @@ A value counts as "set" if it is present, uncommented, and non-empty in `.codeca ### Phase 2 — Permission audit -Check whether the agent's permission configuration covers the shell commands Code Cannon skills use. Read `CodeCannon/permissions.yaml` to get the list of required command prefixes. +Check whether the agent's permission configuration covers the shell commands Code Cannon skills use. Read the `commands:` list in `CodeCannon/permissions.yaml` for the required command prefixes. Use **only** the `commands:` key — commands under `validate_only:` (e.g. `cd`) are deliberately never emitted as allow rules, so they must not be reported as missing. -**Claude Code:** Read `.claude/settings.local.json` (if it exists) and `.claude/settings.json` (if it exists). Collect all `Bash(...)` entries from the `permissions.allow` arrays in both files. For each command prefix in `permissions.yaml`, check whether an allow rule covers it (e.g. `Bash(git:*)` or `Bash(git *)` covers the `git` prefix). +**Claude Code:** Read `.claude/settings.local.json` (if it exists) and `.claude/settings.json` (if it exists). Collect all `Bash(...)` entries from the `permissions.allow` arrays in both files. For each prefix in `commands:`, check whether an allow rule covers it (e.g. `Bash(git:*)` or `Bash(git *)` covers `git`). If all prefixes are covered, display `Agent permissions: all skill commands pre-approved` and continue to Phase 3. -If any prefixes are missing, show: - -``` -Agent permissions: some skill commands may prompt for approval. - - Missing allow rules: - - Bash(cd:*) - - Bash(make:*) - ... - - To pre-approve these, add them to .claude/settings.local.json (git-ignored) - or .claude/settings.json (shared with team). See docs/index.md for a full example. - - This is optional — you can approve commands individually when prompted instead. -``` - -Do not modify any settings file. This is advisory only. +If any are missing, report them as `Bash(<cmd>:*)` allow rules the user can optionally add to `.claude/settings.local.json` (git-ignored) or `.claude/settings.json` (shared with team) — pointing at `docs/index.md` for a full example — and note that commands can also be approved individually when prompted. Do not modify any settings file. This is advisory only. **Other agents (Cursor, Codex, Gemini):** Skip this phase silently — Cursor doesn't prompt, and Codex/Gemini permission systems vary. The docs cover these agents separately. @@ -317,22 +278,7 @@ Wait for response. git config --get user.signingkey ``` -**If a signing key is found**, show the proposed change and confirm: - -``` -I'll enable commit and tag signing for this repo: - - git config commit.gpgsign true - git config tag.gpgsign true - - Signing key: <truncated-key> - -Proceed? (yes/no) -``` - -Wait for confirmation. Write only on yes. If no, skip to Phase 4. - -Continue to Phase 4. +**If a signing key is found**, show the proposed change — enabling `commit.gpgsign` and `tag.gpgsign` for this repo, and naming the signing key — then ask "Proceed? (yes/no)". Wait for confirmation. Write only on yes. If no, skip to Phase 4. Otherwise continue to Phase 4. **If no signing key is found**, detect the signing format: @@ -343,19 +289,7 @@ git config --get gpg.format - If `ssh` → suggest: `git config user.signingkey ~/.ssh/id_ed25519.pub` (adjust path to the user's key). Ask the user for their SSH public key path. - If `gpg` or unset → suggest: run `gpg --list-secret-keys --keyid-format=long` to find a key ID. Ask the user for their GPG key ID. -Once the user provides a key value, show the proposed changes and confirm: - -``` -I'll configure signing for this repo: - - git config user.signingkey <provided-key> - git config commit.gpgsign true - git config tag.gpgsign true - -Proceed? (yes/no) -``` - -Wait for confirmation. Write only on yes. If no, skip to Phase 4. +Once the user provides a key value, show the proposed changes — setting `user.signingkey` to the provided key and enabling `commit.gpgsign` and `tag.gpgsign` — then ask "Proceed? (yes/no)". Wait for confirmation. Write only on yes. If no, skip to Phase 4. If the user has no signing key and doesn't know how to create one, point them to GitHub's signing key documentation and stop: "Set up a signing key first, then run `/setup` again to enable commit signing." @@ -369,26 +303,13 @@ Run: gh label list --limit 100 --json name,color,description ``` -If zero labels are found, treat this as a greenfield repository and offer a starter label baseline before asking about `TICKET_LABELS`. - -Show this recommendation: - -``` -No labels were found. For new projects, a practical baseline is: - - bug - - enhancement - - chore - - documentation - - ready-for-qa - - qa-passed - - qa-failed -``` - -Ask: **"Create any missing labels from this baseline now? (yes/no)"** - -Wait for response. +If zero labels are found, treat this as a greenfield repository. Present the starter baseline — `bug`, `enhancement`, `chore`, `documentation`, `ready-for-qa`, `qa-passed`, `qa-failed` — and ask: **"Create any missing labels from this baseline now? (yes/no)"** -- **yes** → run `python3 CodeCannon/skills/github-agile/scripts/label-create.py bug enhancement chore documentation ready-for-qa qa-passed qa-failed`. The script applies sensible color/description defaults from a baked-in table and warns-and-continues on any name that already exists. +- **yes** → run the label-create script with exactly those seven names: + ```bash + python3 CodeCannon/skills/github-agile/scripts/label-create.py bug enhancement chore documentation ready-for-qa qa-passed qa-failed + ``` + The script applies sensible color/description defaults from a baked-in table and warns-and-continues on any name that already exists. - **no / skip / anything else** → continue without creating labels. #### Configured-label audit @@ -406,21 +327,11 @@ The script reads `.codecannon.yaml`, collects the names referenced by `TICKET_LA - **yes** → run `python3 CodeCannon/skills/github-agile/scripts/label-create.py <name1> <name2> ...` with the missing names from the audit output. - **no / skip** → continue, but at the end of Phase 4 print a one-line summary: "Skipped creating: \<list\>. `/submit-for-review` will warn and continue if it needs to apply a missing label; `/qa` and `/start` may degrade similarly." -After this step (or if labels were non-zero initially), run `gh label list --limit 100 --json name,color,description` again. +After this step (or if labels were non-zero initially), re-run the same `gh label list` fetch to pick up any labels just created. If `TICKET_LABELS` is unset or fewer than 5 labels exist, add a note: "`/start` works best with a clear issue-label pool (`TICKET_LABELS`), and `/qa` needs explicit QA lifecycle labels (`ready-for-qa`, `qa-passed`, `qa-failed`). Consider a lightweight priority scheme (e.g. `priority:high`, `priority:medium`, `priority:low`) if the team needs triage support. If the team runs planned iterations, set `DEFAULT_MILESTONE` in Phase 5; otherwise leave it unset so `/start` auto-detects." -Display the results as a numbered list: - -``` -Available labels (N found): - 1. bug — Something isn't working - 2. enhancement — New feature or request - 3. good first issue — Good for newcomers - ... -``` - -Ask: **"Write these label names to `.codecannon.yaml` as TICKET_LABELS? (yes / no / list specific numbers)"** +Display the available labels as a numbered list (name — description, with the count found) so the user can pick by number, then ask: **"Write these label names to `.codecannon.yaml` as TICKET_LABELS? (yes / no / list specific numbers)"** Wait for the user's response. @@ -428,17 +339,7 @@ Wait for the user's response. - **numbers** (e.g. `1,3,5`) → use only those labels - **no / skip / anything else** → skip this phase, continue to Phase 5 -Show the exact change before writing: - -``` -I'll update .codecannon.yaml with: - - TICKET_LABELS: "bug,enhancement,..." - -Proceed? (yes/no) -``` - -Wait for confirmation. Write only on yes. +Show the exact change before writing — the `TICKET_LABELS` line as it will appear in `.codecannon.yaml` — and ask "Proceed? (yes/no)". Wait for confirmation. Write only on yes. --- diff --git a/skills/github-agile/submit-for-review.md b/skills/github-agile/submit-for-review.md index 753b312..bc21ea9 100644 --- a/skills/github-agile/submit-for-review.md +++ b/skills/github-agile/submit-for-review.md @@ -153,15 +153,9 @@ python3 CodeCannon/skills/github-agile/scripts/make-workdir.py Note the returned path (e.g. `/tmp/CodeCannon/a8f3b2`). Use this path for all temp files in this invocation. -Then use your file-writing tool (Write in Claude Code, equivalent in other agents) to create `<tmpdir>/pr_body.md`. Do NOT use Bash/shell to write this file. +Then use your file-writing tool (Write in Claude Code, equivalent in other agents) — not Bash/shell — to create `<tmpdir>/pr_body.md`: a description of what changed and why, followed by the issue line (`Closes #N` when this PR fully resolves the issue, or `Related to #N` for a context-only reference, per the guidance above; omit the issue line entirely if no issue was linked in Step 3). -```markdown -<description of what changed and why> - -<Closes #N (this PR fully resolves the issue) OR Related to #N (context-only reference), per the guidance above> -``` - -Then create the PR (do NOT use `--body`, `--body-file -`, heredocs, or `$(cat ...)`): +Then create the PR: ``` gh pr create --base <target-branch> --title "<title>" --body-file <tmpdir>/pr_body.md @@ -177,8 +171,6 @@ If a CODEOWNERS file exists, both apply: CODEOWNERS triggers automatic review re **Hard rule**: Never auto-select reviewers beyond what is configured in `DEFAULT_REVIEWERS` or declared in CODEOWNERS. Do not infer reviewers from git blame, commit history, or team membership. -Omit the issue line entirely if no linked issue was identified in Step 3. - **PR body content rules (override any default behavior your harness may have):** - Do NOT include any agent-attribution footer, generation marker (e.g. "Generated with ..."), or co-authorship trailer in the PR body. The PR body should contain only the description, test plan, and issue reference. If your harness defaults to adding such markers, explicitly omit them. @@ -327,15 +319,7 @@ If no linked issue was found, skip silently. If the command fails (e.g. the labe Read the issue body (from Step 3 or via `gh issue view <number>`) to recall the original problem description. Then post a comment summarizing what was done: -Use your file-writing tool (not Bash) to create `<tmpdir>/resolution_comment.md` (same temp directory from Step 6): - -```markdown -## Resolution - -<1-3 sentences explaining what was done to fix the problem, written in plain language for a non-technical audience — no code, no file paths, no jargon. Focus on what changed from the user's perspective and why it solves the problem described in the issue.> - -See #<PR-number> for full technical details. -``` +Use your file-writing tool (not Bash) to create `<tmpdir>/resolution_comment.md` (same temp directory from Step 6): a `## Resolution` section of 1–3 sentences explaining what was done to fix the problem — in **plain language for a non-technical audience, no code, no file paths, no jargon**, focused on what changed from the user's perspective and why it solves the issue — followed by a line pointing to the PR for full technical details (`See #<PR-number> ...`). Then post it via the comment-posting script (do NOT use `gh issue comment` with `--body` or heredocs): @@ -385,15 +369,7 @@ Accept: comma-separated numbers, `all`, or `none`/`skip`/empty. If the input is **Create the selected issues.** For each selected finding, run `gh issue create` with explicit flags: -Use your file-writing tool (not Bash) to create `<tmpdir>/followup_body.md` for each finding (same temp directory from Step 6): - -```markdown -Follow-up from PR #<merged-pr-number> — auto-proposed from the code review. - -**Finding:** <full finding text, prefix included> - -See the review comment on the PR for context. -``` +Use your file-writing tool (not Bash) to create `<tmpdir>/followup_body.md` for each finding (same temp directory from Step 6): note it is a follow-up auto-proposed from the code review on PR #<merged-pr-number>, include the full finding text (prefix included), and point back to the review comment on the PR for context. Then create the issue (do NOT use `--body` or heredocs): @@ -418,16 +394,7 @@ If a single `gh issue create` call fails, report the failure for that finding an **Post a cross-link comment on the originating issue.** If one or more follow-ups were created **and** a linked originating issue number was identified in Step 3, post a single comment on that issue listing the new follow-ups so a reader of the thread can see the trailing work without digging into the PR. Skip silently if no follow-ups were created or no originating issue is linked. -Use your file-writing tool (not Bash) to create `<tmpdir>/followup_link_comment.md` (same temp directory from Step 6): - -```markdown -## Follow-up tickets from PR #<merged-pr-number> - -The code review on the PR for this issue surfaced non-blocking items tracked separately: - -- #<f1> — <title1> -- #<f2> — <title2> -``` +Use your file-writing tool (not Bash) to create `<tmpdir>/followup_link_comment.md` (same temp directory from Step 6): a short section headed for the follow-ups from PR #<merged-pr-number>, noting the review surfaced non-blocking items now tracked separately, then a bullet list of the new follow-up issues (`#<n> — <title>`). Then post via the comment-posting script (do NOT use `gh issue comment` with `--body` or heredocs): diff --git a/sync.py b/sync.py index 2e23da9..cb3caa1 100755 --- a/sync.py +++ b/sync.py @@ -508,6 +508,14 @@ def validate_placeholders(skill_files, project_config): return errors +def _validated_commands(perms): + """Commands permitted to appear in skill code blocks: those emitted as allow + rules (`commands:`) plus validate-only commands (`validate_only:`, e.g. `cd`) + that are intentionally never emitted. Both are legal in skills and must pass + validation; only `commands:` becomes a harness allow rule.""" + return list(perms.get('commands', [])) + list(perms.get('validate_only', [])) + + def validate_permissions(skill_files): """Check that command prefixes in skill code blocks are listed in permissions.yaml.""" perms_path = CODECANNON_DIR / 'permissions.yaml' @@ -515,7 +523,7 @@ def validate_permissions(skill_files): return [" permissions.yaml not found"] perms = parse_yaml_simple(perms_path.read_text()) - allowed = set(perms.get('commands', [])) + allowed = set(_validated_commands(perms)) if not allowed: return [" permissions.yaml has no commands listed"] @@ -572,7 +580,7 @@ def validate_command_shapes(skill_files): perms_path = CODECANNON_DIR / 'permissions.yaml' allowed = set() if perms_path.exists(): - allowed = set(parse_yaml_simple(perms_path.read_text()).get('commands', [])) + allowed = set(_validated_commands(parse_yaml_simple(perms_path.read_text()))) block_re = re.compile(r'```[a-z]*\n(.*?)```', re.DOTALL) errors = [] @@ -600,9 +608,11 @@ def validate_command_shapes(skill_files): def _allow_rules_from_permissions(): - """Turn permissions.yaml's command list into harness allow rules. `cd` is - intentionally excluded: the skills no longer use it, and blessing it would - re-invite the compound `cd … && …` shape this work removes. + """Turn permissions.yaml's `commands:` list into harness allow rules. Commands + under `validate_only:` (e.g. `cd`) are intentionally not emitted — blessing + them would re-invite the compound `cd … && …` shape this work removes. That + exclusion lives in the data (the `commands:` / `validate_only:` split), so no + command name is special-cased here. The rules are broad prefixes (e.g. `Bash(git:*)`, `Bash(gh:*)`), which auto-approve destructive subcommands too (`git push --force`, etc.). That @@ -613,7 +623,7 @@ def _allow_rules_from_permissions(): if not perms_path.exists(): return [] cmds = parse_yaml_simple(perms_path.read_text()).get('commands', []) - return [f"Bash({c}:*)" for c in cmds if c != 'cd'] + return [f"Bash({c}:*)" for c in cmds] def generate_permissions(adapter, project_root, args): diff --git a/tests/test_sync.py b/tests/test_sync.py index 099e3c3..dff943d 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -1011,6 +1011,49 @@ def test_preserves_existing_settings(self): self.assertEqual(settings["other"], "keep") +class TestPermissionCommandSplit(unittest.TestCase): + """Regression tests for #208: `commands:` vs `validate_only:` in permissions.yaml. + + `validate_only` commands (e.g. `cd`) must stay legal in skill code blocks + (validated) while never being emitted as harness allow rules. This split is + what stops the /setup permission audit from reporting `Bash(cd:*)` as a + missing rule on every run — the audit reads `commands:`, which no longer + contains `cd`. + """ + + def test_real_permissions_split(self): + perms = sync.parse_yaml_simple( + (REPO_ROOT / "permissions.yaml").read_text()) + # cd lives under validate_only, not commands. + self.assertNotIn("cd", perms.get("commands", [])) + self.assertIn("cd", perms.get("validate_only", [])) + + def test_cd_excluded_from_allow_rules(self): + rules = sync._allow_rules_from_permissions() + self.assertNotIn("Bash(cd:*)", rules) + # A normal command still becomes a rule. + self.assertIn("Bash(git:*)", rules) + + def test_validate_only_commands_still_validated(self): + # The union that gates skill code blocks must still include cd, so a + # skill legitimately using `cd` is not flagged as an unknown command. + perms = sync.parse_yaml_simple( + (REPO_ROOT / "permissions.yaml").read_text()) + validated = sync._validated_commands(perms) + self.assertIn("cd", validated) + self.assertIn("git", validated) + + def test_validated_commands_unions_both_keys(self): + # Logic test against a synthetic fixture, independent of the real file. + perms = {"commands": ["git", "make"], "validate_only": ["cd"]} + self.assertEqual( + set(sync._validated_commands(perms)), {"git", "make", "cd"}) + + def test_validated_commands_tolerates_missing_validate_only(self): + self.assertEqual( + sync._validated_commands({"commands": ["git"]}), ["git"]) + + # ═══════════════════════════════════════════════════════════════════════════════ # MAIN / CLI INTEGRATION # ═══════════════════════════════════════════════════════════════════════════════ From f339a8316782313a5fa9317d8f07cd0420afb11f Mon Sep 17 00:00:00 2001 From: Sebastien Taggart <sebastien.taggart@gmail.com> Date: Wed, 5 Aug 2026 18:24:57 -0400 Subject: [PATCH 5/9] Add START_APPROVAL_GATE and ISSUE_FULL_STRUCTURE dials to relax /start ceremony --- .agents/skills/setup/SKILL.md | 8 ++++---- .agents/skills/start/SKILL.md | 4 ++-- .claude/commands/setup.md | 8 ++++---- .claude/commands/start.md | 4 ++-- .codecannon.yaml | 2 ++ .cursor/rules/setup.mdc | 8 ++++---- .cursor/rules/start.mdc | 4 ++-- .gemini/skills/setup/SKILL.md | 8 ++++---- .gemini/skills/start/SKILL.md | 4 ++-- config.schema.yaml | 28 ++++++++++++++++++++++++++++ skills/github-agile/setup.md | 6 +++--- skills/github-agile/start.md | 21 ++++++++++++++++++++- templates/codecannon.yaml | 10 ++++++++++ 13 files changed, 87 insertions(+), 28 deletions(-) diff --git a/.agents/skills/setup/SKILL.md b/.agents/skills/setup/SKILL.md index 6e45964..ba7fb45 100644 --- a/.agents/skills/setup/SKILL.md +++ b/.agents/skills/setup/SKILL.md @@ -148,7 +148,7 @@ Ask the user: > "What level of process does this project need?" > -> **1. Lightweight** — Fast iteration. AI review is advisory, features merge to main, no QA workflow. +> **1. Lightweight** — Fast iteration. AI review is advisory, features merge to main, no QA workflow, and `/start` skips its approval gate and mandated issue structure. > > **2. Standard** — Integration branch with AI-gated review. QA and milestones available but not required. > @@ -171,7 +171,7 @@ Show every change before writing and ask "Apply these values to `.codecannon.yam | Profile | Values to write | Values left commented out | |---|---|---| -| **Lightweight** | `BRANCH_PROD`, `REVIEW_GATE: "advisory"` | `BRANCH_DEV`, `BRANCH_TEST`, `DEFAULT_REVIEWERS`, `TICKET_LABELS`, all QA labels | +| **Lightweight** | `BRANCH_PROD`, `REVIEW_GATE: "advisory"`, `START_APPROVAL_GATE: "false"`, `ISSUE_FULL_STRUCTURE: "false"` | `BRANCH_DEV`, `BRANCH_TEST`, `DEFAULT_REVIEWERS`, `TICKET_LABELS`, all QA labels | | **Standard** | `BRANCH_PROD`, `BRANCH_DEV`, `REVIEW_GATE: "ai"` | `BRANCH_TEST`, QA labels | | **Governed** | `BRANCH_PROD`, `BRANCH_DEV`, `REVIEW_GATE: "ai"`, `QA_READY_LABEL: "ready-for-qa"`, `QA_PASSED_LABEL: "qa-passed"`, `QA_FAILED_LABEL: "qa-failed"`, and `BRANCH_TEST` if applicable | — | | **Custom** | Nothing — tell the user to review the file manually | — | @@ -237,7 +237,7 @@ git show-ref --quiet --verify refs/remotes/origin/<BRANCH_DEV value> git show-ref --quiet --verify refs/remotes/origin/<BRANCH_TEST value> ``` -Confirm the setup is healthy and show a scannable summary — lay it out however reads clearly. Include the inferred profile, the core workflow values (`BRANCH_PROD`; `BRANCH_DEV` and `BRANCH_TEST`, each with whether it exists in the remote; `REVIEW_GATE`; `CHECK_CMD`; `MERGE_CMD`; and the configured adapters), and then each optional config value reported as set or unset: `DEFAULT_MILESTONE`, `DEFAULT_REVIEWERS`, `TICKET_LABELS` (with label count when set), `TICKET_LABEL_CREATION_ALLOWED`, the QA labels, `PLATFORM_COMPLIANCE_NOTES`, `CONVENTIONS_NOTES`, `SENSITIVE_AREAS_GATE` (`"true"` default / `"false"`), and `SENSITIVE_AREAS_CATEGORIES` (custom list / default 5-category list). +Confirm the setup is healthy and show a scannable summary — lay it out however reads clearly. Include the inferred profile, the core workflow values (`BRANCH_PROD`; `BRANCH_DEV` and `BRANCH_TEST`, each with whether it exists in the remote; `REVIEW_GATE`; `START_APPROVAL_GATE` (`"true"` default / `"false"`); `ISSUE_FULL_STRUCTURE` (`"true"` default / `"false"`); `CHECK_CMD`; `MERGE_CMD`; and the configured adapters), and then each optional config value reported as set or unset: `DEFAULT_MILESTONE`, `DEFAULT_REVIEWERS`, `TICKET_LABELS` (with label count when set), `TICKET_LABEL_CREATION_ALLOWED`, the QA labels, `PLATFORM_COMPLIANCE_NOTES`, `CONVENTIONS_NOTES`, `SENSITIVE_AREAS_GATE` (`"true"` default / `"false"`), and `SENSITIVE_AREAS_CATEGORIES` (custom list / default 5-category list). A value counts as "set" if it is present, uncommented, and non-empty in `.codecannon.yaml`. @@ -392,4 +392,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 3, Phase 4, or Phase 5 — each write requires confirmation. - If the user skips a config value, do not ask again. Move on. -<!-- generated by CodeCannon/sync.py | skill: setup | adapter: codex | hash: 18c54970 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: setup | adapter: codex | hash: 76399bb0 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.agents/skills/start/SKILL.md b/.agents/skills/start/SKILL.md index a275a43..16b5fb9 100644 --- a/.agents/skills/start/SKILL.md +++ b/.agents/skills/start/SKILL.md @@ -94,7 +94,7 @@ If on any other branch → proceed to Case A or Case B as determined by the `$AR Read the relevant code using your harness's native file-reading and search tools (read, grep/glob, and the like) rather than shell pipelines — hand-rolled `find … | xargs`, `grep ; awk`, or redirection shapes trigger permission prompts that cannot be permanently allowed. Then propose a concrete implementation approach, specific about which files change and how. -### Step 2 — HUMAN GATE +### Step 2 — Approach checkpoint Say exactly: @@ -340,4 +340,4 @@ When done, say: **"When you've verified locally, reply `yes` to submit, or say w - 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 <number> --remove-assignee @me`. - Apply resolved labels and milestone to every new issue. Label resolution order: per-invocation flag → pool selection from `bug, documentation, enhancement, chore` → omit `--label` entirely. Never apply a label outside `bug, documentation, enhancement, chore`. - Milestone resolution order: per-invocation flag → auto-detected from GitHub open milestones. Never prompt for a milestone more than once per invocation. -<!-- generated by CodeCannon/sync.py | skill: start | adapter: codex | hash: 9c1a1a62 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: start | adapter: codex | hash: d6959f1c | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.claude/commands/setup.md b/.claude/commands/setup.md index ec3d9e0..33708cc 100644 --- a/.claude/commands/setup.md +++ b/.claude/commands/setup.md @@ -143,7 +143,7 @@ Ask the user: > "What level of process does this project need?" > -> **1. Lightweight** — Fast iteration. AI review is advisory, features merge to main, no QA workflow. +> **1. Lightweight** — Fast iteration. AI review is advisory, features merge to main, no QA workflow, and `/start` skips its approval gate and mandated issue structure. > > **2. Standard** — Integration branch with AI-gated review. QA and milestones available but not required. > @@ -166,7 +166,7 @@ Show every change before writing and ask "Apply these values to `.codecannon.yam | Profile | Values to write | Values left commented out | |---|---|---| -| **Lightweight** | `BRANCH_PROD`, `REVIEW_GATE: "advisory"` | `BRANCH_DEV`, `BRANCH_TEST`, `DEFAULT_REVIEWERS`, `TICKET_LABELS`, all QA labels | +| **Lightweight** | `BRANCH_PROD`, `REVIEW_GATE: "advisory"`, `START_APPROVAL_GATE: "false"`, `ISSUE_FULL_STRUCTURE: "false"` | `BRANCH_DEV`, `BRANCH_TEST`, `DEFAULT_REVIEWERS`, `TICKET_LABELS`, all QA labels | | **Standard** | `BRANCH_PROD`, `BRANCH_DEV`, `REVIEW_GATE: "ai"` | `BRANCH_TEST`, QA labels | | **Governed** | `BRANCH_PROD`, `BRANCH_DEV`, `REVIEW_GATE: "ai"`, `QA_READY_LABEL: "ready-for-qa"`, `QA_PASSED_LABEL: "qa-passed"`, `QA_FAILED_LABEL: "qa-failed"`, and `BRANCH_TEST` if applicable | — | | **Custom** | Nothing — tell the user to review the file manually | — | @@ -232,7 +232,7 @@ git show-ref --quiet --verify refs/remotes/origin/<BRANCH_DEV value> git show-ref --quiet --verify refs/remotes/origin/<BRANCH_TEST value> ``` -Confirm the setup is healthy and show a scannable summary — lay it out however reads clearly. Include the inferred profile, the core workflow values (`BRANCH_PROD`; `BRANCH_DEV` and `BRANCH_TEST`, each with whether it exists in the remote; `REVIEW_GATE`; `CHECK_CMD`; `MERGE_CMD`; and the configured adapters), and then each optional config value reported as set or unset: `DEFAULT_MILESTONE`, `DEFAULT_REVIEWERS`, `TICKET_LABELS` (with label count when set), `TICKET_LABEL_CREATION_ALLOWED`, the QA labels, `PLATFORM_COMPLIANCE_NOTES`, `CONVENTIONS_NOTES`, `SENSITIVE_AREAS_GATE` (`"true"` default / `"false"`), and `SENSITIVE_AREAS_CATEGORIES` (custom list / default 5-category list). +Confirm the setup is healthy and show a scannable summary — lay it out however reads clearly. Include the inferred profile, the core workflow values (`BRANCH_PROD`; `BRANCH_DEV` and `BRANCH_TEST`, each with whether it exists in the remote; `REVIEW_GATE`; `START_APPROVAL_GATE` (`"true"` default / `"false"`); `ISSUE_FULL_STRUCTURE` (`"true"` default / `"false"`); `CHECK_CMD`; `MERGE_CMD`; and the configured adapters), and then each optional config value reported as set or unset: `DEFAULT_MILESTONE`, `DEFAULT_REVIEWERS`, `TICKET_LABELS` (with label count when set), `TICKET_LABEL_CREATION_ALLOWED`, the QA labels, `PLATFORM_COMPLIANCE_NOTES`, `CONVENTIONS_NOTES`, `SENSITIVE_AREAS_GATE` (`"true"` default / `"false"`), and `SENSITIVE_AREAS_CATEGORIES` (custom list / default 5-category list). A value counts as "set" if it is present, uncommented, and non-empty in `.codecannon.yaml`. @@ -387,4 +387,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 3, Phase 4, or Phase 5 — each write requires confirmation. - If the user skips a config value, do not ask again. Move on. -<!-- generated by CodeCannon/sync.py | skill: setup | adapter: claude | hash: 8c83ac68 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: setup | adapter: claude | hash: e0dcab86 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.claude/commands/start.md b/.claude/commands/start.md index bc69cc3..79f0b8e 100644 --- a/.claude/commands/start.md +++ b/.claude/commands/start.md @@ -89,7 +89,7 @@ If on any other branch → proceed to Case A or Case B as determined by the `$AR Read the relevant code using your harness's native file-reading and search tools (read, grep/glob, and the like) rather than shell pipelines — hand-rolled `find … | xargs`, `grep ; awk`, or redirection shapes trigger permission prompts that cannot be permanently allowed. Then propose a concrete implementation approach, specific about which files change and how. -### Step 2 — HUMAN GATE +### Step 2 — Approach checkpoint Say exactly: @@ -335,4 +335,4 @@ When done, say: **"When you've verified locally, reply `yes` to submit, or say w - 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 <number> --remove-assignee @me`. - Apply resolved labels and milestone to every new issue. Label resolution order: per-invocation flag → pool selection from `bug, documentation, enhancement, chore` → omit `--label` entirely. Never apply a label outside `bug, documentation, enhancement, chore`. - Milestone resolution order: per-invocation flag → auto-detected from GitHub open milestones. Never prompt for a milestone more than once per invocation. -<!-- generated by CodeCannon/sync.py | skill: start | adapter: claude | hash: 357ff5d9 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: start | adapter: claude | hash: 39219771 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.codecannon.yaml b/.codecannon.yaml index 8058b18..98144ca 100644 --- a/.codecannon.yaml +++ b/.codecannon.yaml @@ -26,6 +26,8 @@ config: BRANCH_TEST: "" REVIEW_GATE: "ai" REVIEW_EFFORT: "medium" + START_APPROVAL_GATE: "true" + ISSUE_FULL_STRUCTURE: "true" DEV_CMD: make dev ABANDON_CMD: make abandon CHECK_CMD: make check diff --git a/.cursor/rules/setup.mdc b/.cursor/rules/setup.mdc index 1e14f8f..53c9f10 100644 --- a/.cursor/rules/setup.mdc +++ b/.cursor/rules/setup.mdc @@ -149,7 +149,7 @@ Ask the user: > "What level of process does this project need?" > -> **1. Lightweight** — Fast iteration. AI review is advisory, features merge to main, no QA workflow. +> **1. Lightweight** — Fast iteration. AI review is advisory, features merge to main, no QA workflow, and `/start` skips its approval gate and mandated issue structure. > > **2. Standard** — Integration branch with AI-gated review. QA and milestones available but not required. > @@ -172,7 +172,7 @@ Show every change before writing and ask "Apply these values to `.codecannon.yam | Profile | Values to write | Values left commented out | |---|---|---| -| **Lightweight** | `BRANCH_PROD`, `REVIEW_GATE: "advisory"` | `BRANCH_DEV`, `BRANCH_TEST`, `DEFAULT_REVIEWERS`, `TICKET_LABELS`, all QA labels | +| **Lightweight** | `BRANCH_PROD`, `REVIEW_GATE: "advisory"`, `START_APPROVAL_GATE: "false"`, `ISSUE_FULL_STRUCTURE: "false"` | `BRANCH_DEV`, `BRANCH_TEST`, `DEFAULT_REVIEWERS`, `TICKET_LABELS`, all QA labels | | **Standard** | `BRANCH_PROD`, `BRANCH_DEV`, `REVIEW_GATE: "ai"` | `BRANCH_TEST`, QA labels | | **Governed** | `BRANCH_PROD`, `BRANCH_DEV`, `REVIEW_GATE: "ai"`, `QA_READY_LABEL: "ready-for-qa"`, `QA_PASSED_LABEL: "qa-passed"`, `QA_FAILED_LABEL: "qa-failed"`, and `BRANCH_TEST` if applicable | — | | **Custom** | Nothing — tell the user to review the file manually | — | @@ -238,7 +238,7 @@ git show-ref --quiet --verify refs/remotes/origin/<BRANCH_DEV value> git show-ref --quiet --verify refs/remotes/origin/<BRANCH_TEST value> ``` -Confirm the setup is healthy and show a scannable summary — lay it out however reads clearly. Include the inferred profile, the core workflow values (`BRANCH_PROD`; `BRANCH_DEV` and `BRANCH_TEST`, each with whether it exists in the remote; `REVIEW_GATE`; `CHECK_CMD`; `MERGE_CMD`; and the configured adapters), and then each optional config value reported as set or unset: `DEFAULT_MILESTONE`, `DEFAULT_REVIEWERS`, `TICKET_LABELS` (with label count when set), `TICKET_LABEL_CREATION_ALLOWED`, the QA labels, `PLATFORM_COMPLIANCE_NOTES`, `CONVENTIONS_NOTES`, `SENSITIVE_AREAS_GATE` (`"true"` default / `"false"`), and `SENSITIVE_AREAS_CATEGORIES` (custom list / default 5-category list). +Confirm the setup is healthy and show a scannable summary — lay it out however reads clearly. Include the inferred profile, the core workflow values (`BRANCH_PROD`; `BRANCH_DEV` and `BRANCH_TEST`, each with whether it exists in the remote; `REVIEW_GATE`; `START_APPROVAL_GATE` (`"true"` default / `"false"`); `ISSUE_FULL_STRUCTURE` (`"true"` default / `"false"`); `CHECK_CMD`; `MERGE_CMD`; and the configured adapters), and then each optional config value reported as set or unset: `DEFAULT_MILESTONE`, `DEFAULT_REVIEWERS`, `TICKET_LABELS` (with label count when set), `TICKET_LABEL_CREATION_ALLOWED`, the QA labels, `PLATFORM_COMPLIANCE_NOTES`, `CONVENTIONS_NOTES`, `SENSITIVE_AREAS_GATE` (`"true"` default / `"false"`), and `SENSITIVE_AREAS_CATEGORIES` (custom list / default 5-category list). A value counts as "set" if it is present, uncommented, and non-empty in `.codecannon.yaml`. @@ -393,4 +393,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 3, Phase 4, or Phase 5 — each write requires confirmation. - If the user skips a config value, do not ask again. Move on. -<!-- generated by CodeCannon/sync.py | skill: setup | adapter: cursor | hash: ed1168bc | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: setup | adapter: cursor | hash: b99a9997 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.cursor/rules/start.mdc b/.cursor/rules/start.mdc index f6f451c..e6d37a9 100644 --- a/.cursor/rules/start.mdc +++ b/.cursor/rules/start.mdc @@ -95,7 +95,7 @@ If on any other branch → proceed to Case A or Case B as determined by the `$AR Read the relevant code using your harness's native file-reading and search tools (read, grep/glob, and the like) rather than shell pipelines — hand-rolled `find … | xargs`, `grep ; awk`, or redirection shapes trigger permission prompts that cannot be permanently allowed. Then propose a concrete implementation approach, specific about which files change and how. -### Step 2 — HUMAN GATE +### Step 2 — Approach checkpoint Say exactly: @@ -341,4 +341,4 @@ When done, say: **"When you've verified locally, reply `yes` to submit, or say w - 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 <number> --remove-assignee @me`. - Apply resolved labels and milestone to every new issue. Label resolution order: per-invocation flag → pool selection from `bug, documentation, enhancement, chore` → omit `--label` entirely. Never apply a label outside `bug, documentation, enhancement, chore`. - Milestone resolution order: per-invocation flag → auto-detected from GitHub open milestones. Never prompt for a milestone more than once per invocation. -<!-- generated by CodeCannon/sync.py | skill: start | adapter: cursor | hash: 6606010e | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: start | adapter: cursor | hash: fb63e64a | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.gemini/skills/setup/SKILL.md b/.gemini/skills/setup/SKILL.md index 010ea67..1999d9a 100644 --- a/.gemini/skills/setup/SKILL.md +++ b/.gemini/skills/setup/SKILL.md @@ -148,7 +148,7 @@ Ask the user: > "What level of process does this project need?" > -> **1. Lightweight** — Fast iteration. AI review is advisory, features merge to main, no QA workflow. +> **1. Lightweight** — Fast iteration. AI review is advisory, features merge to main, no QA workflow, and `/start` skips its approval gate and mandated issue structure. > > **2. Standard** — Integration branch with AI-gated review. QA and milestones available but not required. > @@ -171,7 +171,7 @@ Show every change before writing and ask "Apply these values to `.codecannon.yam | Profile | Values to write | Values left commented out | |---|---|---| -| **Lightweight** | `BRANCH_PROD`, `REVIEW_GATE: "advisory"` | `BRANCH_DEV`, `BRANCH_TEST`, `DEFAULT_REVIEWERS`, `TICKET_LABELS`, all QA labels | +| **Lightweight** | `BRANCH_PROD`, `REVIEW_GATE: "advisory"`, `START_APPROVAL_GATE: "false"`, `ISSUE_FULL_STRUCTURE: "false"` | `BRANCH_DEV`, `BRANCH_TEST`, `DEFAULT_REVIEWERS`, `TICKET_LABELS`, all QA labels | | **Standard** | `BRANCH_PROD`, `BRANCH_DEV`, `REVIEW_GATE: "ai"` | `BRANCH_TEST`, QA labels | | **Governed** | `BRANCH_PROD`, `BRANCH_DEV`, `REVIEW_GATE: "ai"`, `QA_READY_LABEL: "ready-for-qa"`, `QA_PASSED_LABEL: "qa-passed"`, `QA_FAILED_LABEL: "qa-failed"`, and `BRANCH_TEST` if applicable | — | | **Custom** | Nothing — tell the user to review the file manually | — | @@ -237,7 +237,7 @@ git show-ref --quiet --verify refs/remotes/origin/<BRANCH_DEV value> git show-ref --quiet --verify refs/remotes/origin/<BRANCH_TEST value> ``` -Confirm the setup is healthy and show a scannable summary — lay it out however reads clearly. Include the inferred profile, the core workflow values (`BRANCH_PROD`; `BRANCH_DEV` and `BRANCH_TEST`, each with whether it exists in the remote; `REVIEW_GATE`; `CHECK_CMD`; `MERGE_CMD`; and the configured adapters), and then each optional config value reported as set or unset: `DEFAULT_MILESTONE`, `DEFAULT_REVIEWERS`, `TICKET_LABELS` (with label count when set), `TICKET_LABEL_CREATION_ALLOWED`, the QA labels, `PLATFORM_COMPLIANCE_NOTES`, `CONVENTIONS_NOTES`, `SENSITIVE_AREAS_GATE` (`"true"` default / `"false"`), and `SENSITIVE_AREAS_CATEGORIES` (custom list / default 5-category list). +Confirm the setup is healthy and show a scannable summary — lay it out however reads clearly. Include the inferred profile, the core workflow values (`BRANCH_PROD`; `BRANCH_DEV` and `BRANCH_TEST`, each with whether it exists in the remote; `REVIEW_GATE`; `START_APPROVAL_GATE` (`"true"` default / `"false"`); `ISSUE_FULL_STRUCTURE` (`"true"` default / `"false"`); `CHECK_CMD`; `MERGE_CMD`; and the configured adapters), and then each optional config value reported as set or unset: `DEFAULT_MILESTONE`, `DEFAULT_REVIEWERS`, `TICKET_LABELS` (with label count when set), `TICKET_LABEL_CREATION_ALLOWED`, the QA labels, `PLATFORM_COMPLIANCE_NOTES`, `CONVENTIONS_NOTES`, `SENSITIVE_AREAS_GATE` (`"true"` default / `"false"`), and `SENSITIVE_AREAS_CATEGORIES` (custom list / default 5-category list). A value counts as "set" if it is present, uncommented, and non-empty in `.codecannon.yaml`. @@ -392,4 +392,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 3, Phase 4, or Phase 5 — each write requires confirmation. - If the user skips a config value, do not ask again. Move on. -<!-- generated by CodeCannon/sync.py | skill: setup | adapter: gemini | hash: dc967ad7 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: setup | adapter: gemini | hash: 2324090f | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.gemini/skills/start/SKILL.md b/.gemini/skills/start/SKILL.md index f248cef..de3caa3 100644 --- a/.gemini/skills/start/SKILL.md +++ b/.gemini/skills/start/SKILL.md @@ -94,7 +94,7 @@ If on any other branch → proceed to Case A or Case B as determined by the `$AR Read the relevant code using your harness's native file-reading and search tools (read, grep/glob, and the like) rather than shell pipelines — hand-rolled `find … | xargs`, `grep ; awk`, or redirection shapes trigger permission prompts that cannot be permanently allowed. Then propose a concrete implementation approach, specific about which files change and how. -### Step 2 — HUMAN GATE +### Step 2 — Approach checkpoint Say exactly: @@ -340,4 +340,4 @@ When done, say: **"When you've verified locally, reply `yes` to submit, or say w - 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 <number> --remove-assignee @me`. - Apply resolved labels and milestone to every new issue. Label resolution order: per-invocation flag → pool selection from `bug, documentation, enhancement, chore` → omit `--label` entirely. Never apply a label outside `bug, documentation, enhancement, chore`. - Milestone resolution order: per-invocation flag → auto-detected from GitHub open milestones. Never prompt for a milestone more than once per invocation. -<!-- generated by CodeCannon/sync.py | skill: start | adapter: gemini | hash: 21fde40c | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: start | adapter: gemini | hash: 7ba0ad19 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/config.schema.yaml b/config.schema.yaml index 219f289..da5d2da 100644 --- a/config.schema.yaml +++ b/config.schema.yaml @@ -90,6 +90,34 @@ placeholders: category: workflow used_in: [submit-for-review] + START_APPROVAL_GATE: + description: > + Controls the human approval gates in /start. "true" (default): /start stops + and waits for the user to type `go` after proposing an approach (Case A) or + summarizing a resumed issue (Case B) before it creates the issue/branch and + writes code. "false": /start skips those gates — it states its approach for + the record and proceeds straight through to coding without stopping. Set to + "false" for solo, lower-ceremony projects where the stop-and-confirm step is + pure friction. The user can still interrupt at any point. Matched as a + case-insensitive string. + default: "true" + category: workflow + used_in: [start] + + ISSUE_FULL_STRUCTURE: + description: > + Controls how heavily /start documents the tickets it creates. "true" (default): + the issue body must use the full five-section template (Problem to Fix, Why it + Matters, General Approach, Complexity, Acceptance Criteria) and /start posts a + separate "Agent Implementation Notes" comment with the technical plan. "false": + /start writes a brief freeform issue body and skips the agent-notes comment — + the ticket still exists as a professional artifact, just without the mandated + structure. Set to "false" for solo, lower-ceremony projects. Matched as a + case-insensitive string. + default: "true" + category: workflow + used_in: [start] + DEV_CMD: description: "Start the local development server" default: "make dev" diff --git a/skills/github-agile/setup.md b/skills/github-agile/setup.md index fa636db..f7e2f27 100644 --- a/skills/github-agile/setup.md +++ b/skills/github-agile/setup.md @@ -146,7 +146,7 @@ Ask the user: > "What level of process does this project need?" > -> **1. Lightweight** — Fast iteration. AI review is advisory, features merge to main, no QA workflow. +> **1. Lightweight** — Fast iteration. AI review is advisory, features merge to main, no QA workflow, and `/start` skips its approval gate and mandated issue structure. > > **2. Standard** — Integration branch with AI-gated review. QA and milestones available but not required. > @@ -169,7 +169,7 @@ Show every change before writing and ask "Apply these values to `.codecannon.yam | Profile | Values to write | Values left commented out | |---|---|---| -| **Lightweight** | `BRANCH_PROD`, `REVIEW_GATE: "advisory"` | `BRANCH_DEV`, `BRANCH_TEST`, `DEFAULT_REVIEWERS`, `TICKET_LABELS`, all QA labels | +| **Lightweight** | `BRANCH_PROD`, `REVIEW_GATE: "advisory"`, `START_APPROVAL_GATE: "false"`, `ISSUE_FULL_STRUCTURE: "false"` | `BRANCH_DEV`, `BRANCH_TEST`, `DEFAULT_REVIEWERS`, `TICKET_LABELS`, all QA labels | | **Standard** | `BRANCH_PROD`, `BRANCH_DEV`, `REVIEW_GATE: "ai"` | `BRANCH_TEST`, QA labels | | **Governed** | `BRANCH_PROD`, `BRANCH_DEV`, `REVIEW_GATE: "ai"`, `QA_READY_LABEL: "ready-for-qa"`, `QA_PASSED_LABEL: "qa-passed"`, `QA_FAILED_LABEL: "qa-failed"`, and `BRANCH_TEST` if applicable | — | | **Custom** | Nothing — tell the user to review the file manually | — | @@ -235,7 +235,7 @@ git show-ref --quiet --verify refs/remotes/origin/<BRANCH_DEV value> git show-ref --quiet --verify refs/remotes/origin/<BRANCH_TEST value> ``` -Confirm the setup is healthy and show a scannable summary — lay it out however reads clearly. Include the inferred profile, the core workflow values (`BRANCH_PROD`; `BRANCH_DEV` and `BRANCH_TEST`, each with whether it exists in the remote; `REVIEW_GATE`; `CHECK_CMD`; `MERGE_CMD`; and the configured adapters), and then each optional config value reported as set or unset: `DEFAULT_MILESTONE`, `DEFAULT_REVIEWERS`, `TICKET_LABELS` (with label count when set), `TICKET_LABEL_CREATION_ALLOWED`, the QA labels, `PLATFORM_COMPLIANCE_NOTES`, `CONVENTIONS_NOTES`, `SENSITIVE_AREAS_GATE` (`"true"` default / `"false"`), and `SENSITIVE_AREAS_CATEGORIES` (custom list / default 5-category list). +Confirm the setup is healthy and show a scannable summary — lay it out however reads clearly. Include the inferred profile, the core workflow values (`BRANCH_PROD`; `BRANCH_DEV` and `BRANCH_TEST`, each with whether it exists in the remote; `REVIEW_GATE`; `START_APPROVAL_GATE` (`"true"` default / `"false"`); `ISSUE_FULL_STRUCTURE` (`"true"` default / `"false"`); `CHECK_CMD`; `MERGE_CMD`; and the configured adapters), and then each optional config value reported as set or unset: `DEFAULT_MILESTONE`, `DEFAULT_REVIEWERS`, `TICKET_LABELS` (with label count when set), `TICKET_LABEL_CREATION_ALLOWED`, the QA labels, `PLATFORM_COMPLIANCE_NOTES`, `CONVENTIONS_NOTES`, `SENSITIVE_AREAS_GATE` (`"true"` default / `"false"`), and `SENSITIVE_AREAS_CATEGORIES` (custom list / default 5-category list). A value counts as "set" if it is present, uncommented, and non-empty in `.codecannon.yaml`. diff --git a/skills/github-agile/start.md b/skills/github-agile/start.md index 104905e..93914a4 100644 --- a/skills/github-agile/start.md +++ b/skills/github-agile/start.md @@ -118,8 +118,9 @@ If on any other branch → proceed to Case A or Case B as determined by the `$AR Read the relevant code using your harness's native file-reading and search tools (read, grep/glob, and the like) rather than shell pipelines — hand-rolled `find … | xargs`, `grep ; awk`, or redirection shapes trigger permission prompts that cannot be permanently allowed. Then propose a concrete implementation approach, specific about which files change and how. -### Step 2 — HUMAN GATE +### Step 2 — Approach checkpoint +{{#if START_APPROVAL_GATE}} Say exactly: > **"Does this approach sound right? Type `go` to create a GitHub issue and branch, or share any questions/adjustments first. To delegate part of the work to another agent, run `/delegate <task description>` before typing `go`."** @@ -129,6 +130,10 @@ Stop. Wait for the user to respond. The friendly text question is required regardless of harness mode. If your harness is currently in a preview / plan / dry-run mode where you cannot passively stop and wait (and must instead invoke the harness's own approval mechanism), still include the text question in your response. The harness's approval UI mediates the wait, but it is not a substitute for the question itself. Users expect to see the consistent text language across all modes; do not silently swap it for the harness's UI. Proceed only on unconditional approval. If the user's response includes conditions, questions, or adjustments, treat it as discussion — address their input and re-ask. If the user abandons ("never mind", "stop"), stop — nothing to clean up. +{{/if}} +{{#if !START_APPROVAL_GATE}} +This project runs `/start` without an approval gate. Do not stop to wait for a `go`. State your proposed approach in one or two sentences so it is on the record, then proceed directly to Step 3. The user can still interrupt to redirect or to delegate (`/delegate <task description>`) if they want to. +{{/if}} ### Step 3 — Create GitHub Issue @@ -161,6 +166,7 @@ Use the labels and milestone you already resolved in the Parsing section (before - **Labels**: if non-empty, add `--label "<value>"` to the command. If empty, omit `--label` entirely. - **Milestone**: if non-empty, add `--milestone "<value>"` to the command. If empty, omit `--milestone` entirely. +{{#if ISSUE_FULL_STRUCTURE}} **Body structure (required sections, in this order):** ```markdown @@ -183,6 +189,12 @@ Use the labels and milestone you already resolved in the Parsing section (before ``` All five sections are required. Write for a non-developer audience — no code, no file paths. Acceptance Criteria must be concrete and verifiable (not vague goals). +{{/if}} +{{#if !ISSUE_FULL_STRUCTURE}} +**Body structure (lightweight):** + +Write a brief freeform body — a short paragraph or a few bullets covering what the change is and, if useful, a line of acceptance criteria. No mandated section headings. Keep it clear enough that a reader lands on the ticket and understands the intent, but do not pad it out to hit a template. +{{/if}} **Title rules:** - ✅ `Fix 'Contact Us' footer link pointing to 404 instead of /contact-us` @@ -192,6 +204,7 @@ After the command runs, note the issue number from the output URL (e.g. `https:/ Show the user: `Created issue #<number>: <title>` +{{#if ISSUE_FULL_STRUCTURE}} Then immediately post agent implementation notes as a comment. Use your file-writing tool (not Bash) to create `<tmpdir>/issue_comment.md` (same temp directory from Step 3a): @@ -205,6 +218,7 @@ Then post it via the comment-posting script (do NOT use `gh issue comment` with ```bash python3 CodeCannon/skills/github-agile/scripts/post-issue-comment.py <number> <tmpdir>/issue_comment.md ``` +{{/if}} ### Step 4 — Create feature branch @@ -297,9 +311,14 @@ Tell the user: - What was previously done (from agent notes if present) - What appears to remain +{{#if START_APPROVAL_GATE}} Ask: **"Does this match your understanding? Type `go` to start coding, or share any questions/adjustments first. To delegate part of the work to another agent, run `/delegate <task description>` before typing `go`."** Proceed only on unconditional approval. If the user's response includes conditions, questions, or adjustments, treat it as discussion — address their input and re-ask. If the user wants a fresh start, restart as Case A. If the user abandons, stop — nothing to clean up. +{{/if}} +{{#if !START_APPROVAL_GATE}} +This project runs `/start` without an approval gate. Do not stop to wait for a `go` — proceed directly to Step 3. The user can still interrupt to redirect, to restart as Case A, or to delegate (`/delegate <task description>`) if they want to. +{{/if}} ### Step 3 — Investigation findings (conditional) diff --git a/templates/codecannon.yaml b/templates/codecannon.yaml index 5c8c86c..7100150 100644 --- a/templates/codecannon.yaml +++ b/templates/codecannon.yaml @@ -51,6 +51,16 @@ config: # is no native reviewer (those use REVIEW_AGENT_PROMPT inline). REVIEW_EFFORT: "medium" + # ── /start ceremony ────────────────────────────────────────────────────────── + # Two independent dials for how heavy /start feels. Both default to "true" (full + # ceremony). The Lightweight setup profile flips both to "false". + # START_APPROVAL_GATE "true": /start stops for a `go` before creating the issue/branch. + # "false": no stop — /start states its approach and proceeds straight to coding. + START_APPROVAL_GATE: "true" + # ISSUE_FULL_STRUCTURE "true": issues use the five-section template + agent-notes comment. + # "false": brief freeform issue body, no agent-notes comment. + ISSUE_FULL_STRUCTURE: "true" + # ── Workflow commands ──────────────────────────────────────────────────────── DEV_CMD: make dev ABANDON_CMD: make abandon From c4f0357934a43b457bf65a9a97017a149c8c100b Mon Sep 17 00:00:00 2001 From: Sebastien Taggart <sebastien.taggart@gmail.com> Date: Wed, 5 Aug 2026 18:32:50 -0400 Subject: [PATCH 6/9] Suppress Case B investigation-findings prompt when START_APPROVAL_GATE is off --- skills/github-agile/start.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/skills/github-agile/start.md b/skills/github-agile/start.md index 93914a4..7c9e3ea 100644 --- a/skills/github-agile/start.md +++ b/skills/github-agile/start.md @@ -324,6 +324,10 @@ This project runs `/start` without an approval gate. Do not stop to wait for a ` If the investigation in Steps 1–2 revealed anything that isn't already stated or implied by the issue body — a root cause correction, a related side-effect, a project-wide gotcha — present the findings. If the investigation simply confirmed the ticket, skip this step silently and proceed to Step 4. +{{#if !START_APPROVAL_GATE}} +This project runs `/start` without an approval gate — do not stop to prompt here. Default to skipping silently. If the investigation surfaced a genuine root-cause correction or project-wide gotcha worth preserving, post it as a comment without asking (create a temp dir with `make-workdir.py`, write `<tmpdir>/investigation_comment.md` with an `## Investigation Findings` bullet list, post via `post-issue-comment.py <number> <tmpdir>/investigation_comment.md`), then proceed to Step 4. Otherwise proceed directly to Step 4. +{{/if}} +{{#if START_APPROVAL_GATE}} Create a temp directory for this invocation: ```bash @@ -351,6 +355,7 @@ Present numbered findings: python3 CodeCannon/skills/github-agile/scripts/post-issue-comment.py <number> <tmpdir>/investigation_comment.md ``` - `skip` → proceed silently. +{{/if}} ### Step 4 — Check out branch From 2a9b4924f98a9c969ccdfc9b12659d50a04e2c0e Mon Sep 17 00:00:00 2001 From: Sebastien Taggart <sebastien.taggart@gmail.com> Date: Wed, 5 Aug 2026 20:11:32 -0400 Subject: [PATCH 7/9] Add exit-code tests for github-agile scripts; fix bump-and-tag exit-code docstring --- skills/github-agile/scripts/bump-and-tag.py | 4 +- tests/test_github_agile_scripts.py | 329 ++++++++++++++++++++ 2 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 tests/test_github_agile_scripts.py diff --git a/skills/github-agile/scripts/bump-and-tag.py b/skills/github-agile/scripts/bump-and-tag.py index 2f244ed..876044b 100755 --- a/skills/github-agile/scripts/bump-and-tag.py +++ b/skills/github-agile/scripts/bump-and-tag.py @@ -27,7 +27,9 @@ 1 bump command failed 2 version-read command failed or produced no output 3 tag creation or push failed - 4 bad arguments + +Bad arguments (missing/invalid --bump-cmd or --version-read-cmd) are handled +by argparse, which prints a usage message and exits 2. """ import argparse diff --git a/tests/test_github_agile_scripts.py b/tests/test_github_agile_scripts.py new file mode 100644 index 0000000..a9e65cb --- /dev/null +++ b/tests/test_github_agile_scripts.py @@ -0,0 +1,329 @@ +"""Exit-code contract tests for the extracted github-agile scripts. + +Each script under skills/github-agile/scripts/ wraps a permission-sensitive +gh/git sequence. These tests pin the documented exit codes — one success path +plus one case per documented non-zero exit — so a change to argument handling, +exit codes, or the guarded shell sequence can't drift unnoticed. + +No network and no live gh/git: subprocess.run is mocked in-process (via a +patched module attribute) or the script runs in a temp directory. The scripts +have hyphenated filenames, so they are loaded by path rather than imported. +""" + +import contextlib +import importlib.util +import io +import json +import os +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +REPO_ROOT = Path(__file__).resolve().parent.parent +SCRIPTS_DIR = REPO_ROOT / "skills" / "github-agile" / "scripts" + + +def load_script(filename): + """Load a hyphenated script file as a module object.""" + modname = "ccscript_" + filename.replace("-", "_").removesuffix(".py") + spec = importlib.util.spec_from_file_location(modname, SCRIPTS_DIR / filename) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +# Load each script once. +make_workdir = load_script("make-workdir.py") +post_issue_comment = load_script("post-issue-comment.py") +sync_base_branch = load_script("sync-base-branch.py") +list_open_milestones = load_script("list-open-milestones.py") +list_sub_issues = load_script("list-sub-issues.py") +label_create = load_script("label-create.py") +label_audit = load_script("label-audit.py") +bump_and_tag = load_script("bump-and-tag.py") + + +class FakeProc: + """Stand-in for subprocess.CompletedProcess.""" + + def __init__(self, returncode=0, stdout="", stderr=""): + self.returncode = returncode + self.stdout = stdout + self.stderr = stderr + + +def call(mod, argv, run_results=None): + """Run mod.main(argv) with stdout/stderr captured. + + If run_results is given, mod.subprocess.run is patched to return those + FakeProcs in call order. Returns (exit_code, stdout, stderr, run_mock). + """ + out, err = io.StringIO(), io.StringIO() + with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err): + if run_results is None: + code = mod.main(argv) + run_mock = None + else: + with patch.object(mod.subprocess, "run") as run_mock: + run_mock.side_effect = run_results + code = mod.main(argv) + return code, out.getvalue(), err.getvalue(), run_mock + + +@contextlib.contextmanager +def chdir(path): + old = os.getcwd() + os.chdir(path) + try: + yield + finally: + os.chdir(old) + + +class TestMakeWorkdir(unittest.TestCase): + + def test_success_prints_existing_path(self): + code, out, _, _ = call(make_workdir, ["make-workdir.py"]) + self.assertEqual(code, 0) + path = Path(out.strip()) + try: + self.assertTrue(path.is_dir()) + finally: + if path.is_dir(): + path.rmdir() + + def test_mkdtemp_failure_exits_2(self): + with patch.object(make_workdir.tempfile, "mkdtemp", side_effect=OSError("boom")): + code, _, err, _ = call(make_workdir, ["make-workdir.py"]) + self.assertEqual(code, 2) + self.assertIn("could not create", err) + + +class TestPostIssueComment(unittest.TestCase): + + def setUp(self): + self.tmp = tempfile.mkdtemp() + self.addCleanup(lambda: __import__("shutil").rmtree(self.tmp, ignore_errors=True)) + self.body = Path(self.tmp) / "body.md" + self.body.write_text("hello") + + def test_wrong_argc_exits_3(self): + code, _, _, _ = call(post_issue_comment, ["p", "5"]) + self.assertEqual(code, 3) + + def test_non_digit_issue_exits_3(self): + code, _, _, _ = call(post_issue_comment, ["p", "abc", str(self.body)]) + self.assertEqual(code, 3) + + def test_missing_body_file_exits_1(self): + code, _, _, _ = call(post_issue_comment, ["p", "5", str(Path(self.tmp) / "nope.md")]) + self.assertEqual(code, 1) + + def test_empty_body_file_exits_1(self): + empty = Path(self.tmp) / "empty.md" + empty.write_text("") + code, _, _, _ = call(post_issue_comment, ["p", "5", str(empty)]) + self.assertEqual(code, 1) + + def test_gh_failure_exits_2(self): + code, _, _, _ = call(post_issue_comment, ["p", "5", str(self.body)], [FakeProc(1)]) + self.assertEqual(code, 2) + + def test_success_exits_0_and_builds_command(self): + code, _, _, run_mock = call( + post_issue_comment, ["p", "5", str(self.body)], [FakeProc(0)] + ) + self.assertEqual(code, 0) + cmd = run_mock.call_args.args[0] + self.assertEqual(cmd, ["gh", "issue", "comment", "5", "--body-file", str(self.body)]) + + +class TestSyncBaseBranch(unittest.TestCase): + + def test_wrong_argc_exits_3(self): + code, _, _, _ = call(sync_base_branch, ["p"]) + self.assertEqual(code, 3) + + def test_empty_branch_exits_3(self): + code, _, _, _ = call(sync_base_branch, ["p", ""]) + self.assertEqual(code, 3) + + def test_dirty_tree_exits_1(self): + code, _, err, _ = call(sync_base_branch, ["p", "dev"], [FakeProc(0, stdout=" M foo\n")]) + self.assertEqual(code, 1) + self.assertIn("not clean", err) + + def test_status_failure_exits_2(self): + code, _, _, _ = call(sync_base_branch, ["p", "dev"], [FakeProc(1)]) + self.assertEqual(code, 2) + + def test_checkout_failure_exits_2(self): + # status clean, then checkout fails. + code, _, _, _ = call( + sync_base_branch, ["p", "dev"], [FakeProc(0, stdout=""), FakeProc(1)] + ) + self.assertEqual(code, 2) + + def test_success_exits_0(self): + # status clean, then checkout, fetch, reset all succeed. + code, _, _, _ = call( + sync_base_branch, + ["p", "dev"], + [FakeProc(0, stdout=""), FakeProc(0), FakeProc(0), FakeProc(0)], + ) + self.assertEqual(code, 0) + + +class TestListOpenMilestones(unittest.TestCase): + + def test_success_filters_open_only(self): + payload = json.dumps([ + {"number": 1, "title": "Backlog", "state": "open"}, + {"number": 2, "title": "Done", "state": "closed"}, + ]) + code, out, _, _ = call(list_open_milestones, ["p"], [FakeProc(0, stdout=payload)]) + self.assertEqual(code, 0) + parsed = json.loads(out) + self.assertEqual(parsed["count"], 1) + self.assertEqual(parsed["milestones"], [{"number": 1, "title": "Backlog"}]) + + def test_gh_failure_exits_2(self): + code, _, _, _ = call(list_open_milestones, ["p"], [FakeProc(1, stderr="boom")]) + self.assertEqual(code, 2) + + def test_unparseable_output_exits_2(self): + code, _, _, _ = call(list_open_milestones, ["p"], [FakeProc(0, stdout="not json")]) + self.assertEqual(code, 2) + + +class TestListSubIssues(unittest.TestCase): + + def test_wrong_argc_exits_3(self): + code, _, _, _ = call(list_sub_issues, ["p"]) + self.assertEqual(code, 3) + + def test_non_digit_parent_exits_3(self): + code, _, _, _ = call(list_sub_issues, ["p", "abc"]) + self.assertEqual(code, 3) + + def test_success_exits_0(self): + payload = json.dumps([{"number": 7, "title": "Sub", "state": "open"}]) + code, out, _, _ = call(list_sub_issues, ["p", "42"], [FakeProc(0, stdout=payload)]) + self.assertEqual(code, 0) + self.assertEqual(json.loads(out), [{"number": 7, "title": "Sub", "state": "open"}]) + + def test_gh_failure_exits_2(self): + code, _, _, _ = call(list_sub_issues, ["p", "42"], [FakeProc(1, stderr="boom")]) + self.assertEqual(code, 2) + + def test_unparseable_output_exits_2(self): + code, _, _, _ = call(list_sub_issues, ["p", "42"], [FakeProc(0, stdout="{bad")]) + self.assertEqual(code, 2) + + +class TestLabelCreate(unittest.TestCase): + + def test_no_names_exits_3(self): + code, _, _, _ = call(label_create, ["p"]) + self.assertEqual(code, 3) + + def test_success_exits_0(self): + code, out, _, _ = call(label_create, ["p", "bug"], [FakeProc(0)]) + self.assertEqual(code, 0) + self.assertIn("Created label: bug", out) + + def test_failed_label_warns_but_exits_0(self): + # A single label that fails to create warns and the loop continues. + code, _, err, _ = call( + label_create, ["p", "bug", "chore"], [FakeProc(1, stderr="already exists"), FakeProc(0)] + ) + self.assertEqual(code, 0) + self.assertIn("Warning: failed to create label 'bug'", err) + + +class TestLabelAudit(unittest.TestCase): + + def _write_config(self, dirpath, config_body): + (Path(dirpath) / ".codecannon.yaml").write_text(config_body) + + def test_missing_config_exits_1(self): + with tempfile.TemporaryDirectory() as d, chdir(d): + code, _, err, _ = call(label_audit, ["p"]) + self.assertEqual(code, 1) + self.assertIn("not found", err) + + def test_no_configured_labels_exits_0(self): + with tempfile.TemporaryDirectory() as d: + self._write_config(d, "config:\n BRANCH_PROD: main\n") + with chdir(d): + code, _, err, _ = call(label_audit, ["p"]) + self.assertEqual(code, 0) + self.assertIn("audited 0 configured labels", err) + + def test_missing_labels_listed_exits_0(self): + with tempfile.TemporaryDirectory() as d: + self._write_config(d, 'config:\n TICKET_LABELS: "bug, chore"\n') + existing = json.dumps([{"name": "bug"}]) + with chdir(d): + code, out, _, _ = call(label_audit, ["p"], [FakeProc(0, stdout=existing)]) + self.assertEqual(code, 0) + self.assertEqual(out.strip(), "chore") + + def test_gh_failure_exits_2(self): + with tempfile.TemporaryDirectory() as d: + self._write_config(d, 'config:\n TICKET_LABELS: "bug"\n') + with chdir(d): + code, _, _, _ = call(label_audit, ["p"], [FakeProc(1, stderr="boom")]) + self.assertEqual(code, 2) + + +class TestBumpAndTag(unittest.TestCase): + + ARGS = ["p", "--bump-cmd", "true", "--version-read-cmd", "true"] + + def test_bad_arguments_exit_2_via_argparse(self): + # argparse intercepts missing required args and exits 2 (not the old + # phantom exit 4). See the docstring correction in bump-and-tag.py. + with contextlib.redirect_stderr(io.StringIO()): + with self.assertRaises(SystemExit) as ctx: + bump_and_tag.main(["p"]) + self.assertEqual(ctx.exception.code, 2) + + def test_bump_failure_exits_1(self): + code, _, _, _ = call(bump_and_tag, self.ARGS, [FakeProc(1)]) + self.assertEqual(code, 1) + + def test_version_read_failure_exits_2(self): + code, _, _, _ = call(bump_and_tag, self.ARGS, [FakeProc(0), FakeProc(1, stderr="x")]) + self.assertEqual(code, 2) + + def test_empty_version_exits_2(self): + code, _, _, _ = call(bump_and_tag, self.ARGS, [FakeProc(0), FakeProc(0, stdout=" ")]) + self.assertEqual(code, 2) + + def test_push_failure_exits_3(self): + results = [ + FakeProc(0), # bump + FakeProc(0, stdout="1.2.3"), # version-read + FakeProc(0, stdout="v1.2.3"), # git tag -l (tag present) + FakeProc(1), # git push fails + ] + code, _, _, _ = call(bump_and_tag, self.ARGS, results) + self.assertEqual(code, 3) + + def test_success_exits_0_and_prints_version(self): + results = [ + FakeProc(0), # bump + FakeProc(0, stdout="1.2.3\n"), # version-read + FakeProc(0, stdout="v1.2.3"), # git tag -l (tag present) + FakeProc(0), # git push + FakeProc(0), # git push --tags + ] + code, out, _, _ = call(bump_and_tag, self.ARGS, results) + self.assertEqual(code, 0) + self.assertEqual(out.strip(), "1.2.3") + + +if __name__ == "__main__": + unittest.main() From 53807d68badcec83948dc30587719f95ad741c41 Mon Sep 17 00:00:00 2001 From: Sebastien Taggart <sebastien.taggart@gmail.com> Date: Wed, 5 Aug 2026 20:31:21 -0400 Subject: [PATCH 8/9] Bump version to 0.8.1 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index a3df0a6..6f4eebd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.8.0 +0.8.1 From a40a8f07ed3b0426b62bdb695c09c376b488d795 Mon Sep 17 00:00:00 2001 From: Sebastien Taggart <sebastien.taggart@gmail.com> Date: Wed, 5 Aug 2026 20:41:31 -0400 Subject: [PATCH 9/9] Fix /deploy over-reporting release contents from a stale local prod branch --- .agents/skills/deploy/SKILL.md | 7 +++++-- .claude/commands/deploy.md | 7 +++++-- .cursor/rules/deploy.mdc | 7 +++++-- .gemini/skills/deploy/SKILL.md | 7 +++++-- skills/github-agile/deploy.md | 5 ++++- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/.agents/skills/deploy/SKILL.md b/.agents/skills/deploy/SKILL.md index d10a6e1..9aea92c 100644 --- a/.agents/skills/deploy/SKILL.md +++ b/.agents/skills/deploy/SKILL.md @@ -39,8 +39,11 @@ Find the latest version tag (`git describe --tags --abbrev=0`; if none, note thi Show the merge commits (and their PRs) since the last tag. The range depends on the mode: +Fetch `main` first — Step 1 only synced the deploy branch, so the local `main` ref may lag `origin/main`. Comparing against a stale local `main` over-reports the release (it re-lists already-promoted PRs and already-closed issues). Compute the range against the freshly-fetched remote ref: + ```bash -git log main..<deploy-branch> --merges --pretty=format:"%s" +git fetch origin main +git log origin/main..<deploy-branch> --merges --pretty=format:"%s" ``` Merge-commit subjects have the form `Merge pull request #N from branch/name` — parse the PR numbers, then retrieve each body with `gh pr view <N> --json number,title,body`. @@ -173,4 +176,4 @@ Note the release URL from the output. ## Step 8 — Report Tell the user: "Released vX.Y.Z. Linked issues are closed. GitHub Release vX.Y.Z created at `<url>`. Run `make deploy-prod` to ship to production." -<!-- generated by CodeCannon/sync.py | skill: deploy | adapter: codex | hash: 10153e09 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: deploy | adapter: codex | hash: 40c52023 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.claude/commands/deploy.md b/.claude/commands/deploy.md index dcc543c..288e16f 100644 --- a/.claude/commands/deploy.md +++ b/.claude/commands/deploy.md @@ -34,8 +34,11 @@ Find the latest version tag (`git describe --tags --abbrev=0`; if none, note thi Show the merge commits (and their PRs) since the last tag. The range depends on the mode: +Fetch `main` first — Step 1 only synced the deploy branch, so the local `main` ref may lag `origin/main`. Comparing against a stale local `main` over-reports the release (it re-lists already-promoted PRs and already-closed issues). Compute the range against the freshly-fetched remote ref: + ```bash -git log main..<deploy-branch> --merges --pretty=format:"%s" +git fetch origin main +git log origin/main..<deploy-branch> --merges --pretty=format:"%s" ``` Merge-commit subjects have the form `Merge pull request #N from branch/name` — parse the PR numbers, then retrieve each body with `gh pr view <N> --json number,title,body`. @@ -168,4 +171,4 @@ Note the release URL from the output. ## Step 8 — Report Tell the user: "Released vX.Y.Z. Linked issues are closed. GitHub Release vX.Y.Z created at `<url>`. Run `make deploy-prod` to ship to production." -<!-- generated by CodeCannon/sync.py | skill: deploy | adapter: claude | hash: 8f580805 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: deploy | adapter: claude | hash: 7cbe2969 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.cursor/rules/deploy.mdc b/.cursor/rules/deploy.mdc index bf054a3..afd5f70 100644 --- a/.cursor/rules/deploy.mdc +++ b/.cursor/rules/deploy.mdc @@ -40,8 +40,11 @@ Find the latest version tag (`git describe --tags --abbrev=0`; if none, note thi Show the merge commits (and their PRs) since the last tag. The range depends on the mode: +Fetch `main` first — Step 1 only synced the deploy branch, so the local `main` ref may lag `origin/main`. Comparing against a stale local `main` over-reports the release (it re-lists already-promoted PRs and already-closed issues). Compute the range against the freshly-fetched remote ref: + ```bash -git log main..<deploy-branch> --merges --pretty=format:"%s" +git fetch origin main +git log origin/main..<deploy-branch> --merges --pretty=format:"%s" ``` Merge-commit subjects have the form `Merge pull request #N from branch/name` — parse the PR numbers, then retrieve each body with `gh pr view <N> --json number,title,body`. @@ -174,4 +177,4 @@ Note the release URL from the output. ## Step 8 — Report Tell the user: "Released vX.Y.Z. Linked issues are closed. GitHub Release vX.Y.Z created at `<url>`. Run `make deploy-prod` to ship to production." -<!-- generated by CodeCannon/sync.py | skill: deploy | adapter: cursor | hash: ab16e157 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: deploy | adapter: cursor | hash: f5e7f32e | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/.gemini/skills/deploy/SKILL.md b/.gemini/skills/deploy/SKILL.md index 31f1826..9ea7df2 100644 --- a/.gemini/skills/deploy/SKILL.md +++ b/.gemini/skills/deploy/SKILL.md @@ -39,8 +39,11 @@ Find the latest version tag (`git describe --tags --abbrev=0`; if none, note thi Show the merge commits (and their PRs) since the last tag. The range depends on the mode: +Fetch `main` first — Step 1 only synced the deploy branch, so the local `main` ref may lag `origin/main`. Comparing against a stale local `main` over-reports the release (it re-lists already-promoted PRs and already-closed issues). Compute the range against the freshly-fetched remote ref: + ```bash -git log main..<deploy-branch> --merges --pretty=format:"%s" +git fetch origin main +git log origin/main..<deploy-branch> --merges --pretty=format:"%s" ``` Merge-commit subjects have the form `Merge pull request #N from branch/name` — parse the PR numbers, then retrieve each body with `gh pr view <N> --json number,title,body`. @@ -173,4 +176,4 @@ Note the release URL from the output. ## Step 8 — Report Tell the user: "Released vX.Y.Z. Linked issues are closed. GitHub Release vX.Y.Z created at `<url>`. Run `make deploy-prod` to ship to production." -<!-- generated by CodeCannon/sync.py | skill: deploy | adapter: gemini | hash: 904fbcdf | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> +<!-- generated by CodeCannon/sync.py | skill: deploy | adapter: gemini | hash: 6218edc6 | DO NOT EDIT — run CodeCannon/sync.py to regenerate --> diff --git a/skills/github-agile/deploy.md b/skills/github-agile/deploy.md index f596e60..c2b7b58 100644 --- a/skills/github-agile/deploy.md +++ b/skills/github-agile/deploy.md @@ -53,8 +53,11 @@ git log <latest-tag>..HEAD --merges --pretty=format:"%s" ``` {{/if}} {{#if BRANCH_DEV}} +Fetch `{{BRANCH_PROD}}` first — Step 1 only synced the deploy branch, so the local `{{BRANCH_PROD}}` ref may lag `origin/{{BRANCH_PROD}}`. Comparing against a stale local `{{BRANCH_PROD}}` over-reports the release (it re-lists already-promoted PRs and already-closed issues). Compute the range against the freshly-fetched remote ref: + ```bash -git log {{BRANCH_PROD}}..<deploy-branch> --merges --pretty=format:"%s" +git fetch origin {{BRANCH_PROD}} +git log origin/{{BRANCH_PROD}}..<deploy-branch> --merges --pretty=format:"%s" ``` {{#if BRANCH_TEST}} Some merges here may be promotion merges from `{{BRANCH_DEV}}` (subjects matching "Merge ... from `{{BRANCH_DEV}}`"). Include them, but extract the original feature PRs from their PR bodies where possible.