project-init 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- project_init/__init__.py +4 -0
- project_init/__main__.py +662 -0
- project_init/mcps.py +57 -0
- project_init/scaffold.py +374 -0
- project_init/templates/base/AGENTS.md.tmpl +50 -0
- project_init/templates/base/CLAUDE.md.tmpl +16 -0
- project_init/templates/base/CONTRIBUTING.md.tmpl +55 -0
- project_init/templates/base/GEMINI.md.tmpl +16 -0
- project_init/templates/base/LICENSE.tmpl +231 -0
- project_init/templates/base/SECURITY.md.tmpl +26 -0
- project_init/templates/base/docs/explanation/index.md +9 -0
- project_init/templates/base/docs/how-to/index.md +7 -0
- project_init/templates/base/docs/index.md.tmpl +20 -0
- project_init/templates/base/docs/reference/index.md +13 -0
- project_init/templates/base/docs/tutorials/index.md +7 -0
- project_init/templates/base/dot_claude/agents/README.md +30 -0
- project_init/templates/base/dot_claude/config.yaml.tmpl +31 -0
- project_init/templates/base/dot_claude/docs/README.md +26 -0
- project_init/templates/base/dot_claude/docs/adr/adr-001-memory-stack.md.tmpl +22 -0
- project_init/templates/base/dot_claude/docs/adr/adr-002-mcp-choices.md.tmpl +32 -0
- project_init/templates/base/dot_claude/docs/adr/adr-template.md +29 -0
- project_init/templates/base/dot_claude/docs/development/conventions.md.tmpl +31 -0
- project_init/templates/base/dot_claude/docs/development/testing.md +25 -0
- project_init/templates/base/dot_claude/docs/guides/developer-onboarding.md +110 -0
- project_init/templates/base/dot_claude/docs/guides/issue-metadata.md +27 -0
- project_init/templates/base/dot_claude/docs/guides/secrets.md +50 -0
- project_init/templates/base/dot_claude/docs/guides/using-memory.md +36 -0
- project_init/templates/base/dot_claude/hooks/README.md +15 -0
- project_init/templates/base/dot_claude/hooks/agent_guard_adapter.py.tmpl +64 -0
- project_init/templates/base/dot_claude/hooks/dag_workflow.py +610 -0
- project_init/templates/base/dot_claude/memory/MEMORY.md.tmpl +11 -0
- project_init/templates/base/dot_claude/memory/README.md +51 -0
- project_init/templates/base/dot_claude/memory/SCHEMA.md +52 -0
- project_init/templates/base/dot_claude/memory/feedback_conventions.md +11 -0
- project_init/templates/base/dot_claude/memory/project_context.md.tmpl +11 -0
- project_init/templates/base/dot_claude/memory/user_role.md +7 -0
- project_init/templates/base/dot_claude/project-init.md.tmpl +174 -0
- project_init/templates/base/dot_claude/rules/go.md +14 -0
- project_init/templates/base/dot_claude/rules/hooks.md +30 -0
- project_init/templates/base/dot_claude/rules/node.md +17 -0
- project_init/templates/base/dot_claude/rules/python.md +25 -0
- project_init/templates/base/dot_claude/scripts/README.md +15 -0
- project_init/templates/base/dot_claude/scripts/create_issue.sh +577 -0
- project_init/templates/base/dot_claude/scripts/create_nojira_pr.sh +3 -0
- project_init/templates/base/dot_claude/scripts/finish_pr.sh +3 -0
- project_init/templates/base/dot_claude/scripts/install_hooks.sh +55 -0
- project_init/templates/base/dot_claude/scripts/monitor_pr.sh +270 -0
- project_init/templates/base/dot_claude/scripts/promote_review.sh +3 -0
- project_init/templates/base/dot_claude/scripts/push_branch.sh +5 -0
- project_init/templates/base/dot_claude/scripts/push_wiki.sh +34 -0
- project_init/templates/base/dot_claude/scripts/setup_github.sh +219 -0
- project_init/templates/base/dot_claude/scripts/start_issue.sh +134 -0
- project_init/templates/base/dot_claude/settings.json.tmpl +83 -0
- project_init/templates/base/dot_claude/skills/README.md +12 -0
- project_init/templates/base/dot_claude/skills/plan/SKILL.md.tmpl +40 -0
- project_init/templates/base/dot_claude/vault/README.md +21 -0
- project_init/templates/base/dot_claude/vault/decisions/README.md +22 -0
- project_init/templates/base/dot_claude/vault/design/README.md +3 -0
- project_init/templates/base/dot_claude/vault/knowledge/README.md +5 -0
- project_init/templates/base/dot_claude/vault/sessions/README.md +5 -0
- project_init/templates/base/dot_devcontainer/devcontainer.json.tmpl +17 -0
- project_init/templates/base/dot_devcontainer/post-create.sh.tmpl +31 -0
- project_init/templates/base/dot_env.example.tmpl +13 -0
- project_init/templates/base/dot_github/CODEOWNERS.tmpl +12 -0
- project_init/templates/base/dot_github/ISSUE_TEMPLATE/bug.yml +98 -0
- project_init/templates/base/dot_github/ISSUE_TEMPLATE/chore.yml +82 -0
- project_init/templates/base/dot_github/ISSUE_TEMPLATE/config.yml +5 -0
- project_init/templates/base/dot_github/ISSUE_TEMPLATE/docs.yml +84 -0
- project_init/templates/base/dot_github/ISSUE_TEMPLATE/feature.yml +87 -0
- project_init/templates/base/dot_github/ISSUE_TEMPLATE/test.yml +90 -0
- project_init/templates/base/dot_github/copilot-instructions.md.tmpl +25 -0
- project_init/templates/base/dot_github/hooks/commit-msg +52 -0
- project_init/templates/base/dot_github/hooks/pre-commit +16 -0
- project_init/templates/base/dot_github/hooks/pre-push +51 -0
- project_init/templates/base/dot_github/pull_request_template.md +22 -0
- project_init/templates/base/dot_github/workflows/board-automation.yml +232 -0
- project_init/templates/base/dot_github/workflows/ci.yml.tmpl +204 -0
- project_init/templates/base/dot_github/workflows/docs.yml.tmpl +98 -0
- project_init/templates/base/dot_github/workflows/issue-validation.yml +72 -0
- project_init/templates/base/dot_github/workflows/review-status.yml +48 -0
- project_init/templates/base/dot_github/workflows/validate-pr.yml +103 -0
- project_init/templates/base/dot_gitignore.tmpl +41 -0
- project_init/templates/base/dot_golangci.yml.tmpl +20 -0
- project_init/templates/base/dot_vscode/extensions.json.tmpl +10 -0
- project_init/templates/base/dot_vscode/settings.json.tmpl +8 -0
- project_init/templates/base/eslint.config.mjs.tmpl +29 -0
- project_init/templates/base/justfile.tmpl +95 -0
- project_init/templates/base/mise.toml.tmpl +20 -0
- project_init/templates/base/mkdocs.yml.tmpl +32 -0
- project_init/templates/base/renovate.json +14 -0
- project_init/templates/base/ruff.toml.tmpl +31 -0
- project_init/templates/base/typedoc.json.tmpl +14 -0
- project_init/templates/codex/dot_agents/skills/add_adr/SKILL.md +33 -0
- project_init/templates/codex/dot_agents/skills/add_command/SKILL.md +63 -0
- project_init/templates/codex/dot_agents/skills/add_hook/SKILL.md +112 -0
- project_init/templates/codex/dot_agents/skills/audit/SKILL.md +146 -0
- project_init/templates/codex/dot_agents/skills/create_issue/SKILL.md +59 -0
- project_init/templates/codex/dot_agents/skills/github_workflow/SKILL.md +80 -0
- project_init/templates/codex/dot_agents/skills/request_review/SKILL.md +19 -0
- project_init/templates/codex/dot_agents/skills/review/SKILL.md +17 -0
- project_init/templates/codex/dot_agents/skills/save_memory/SKILL.md +17 -0
- project_init/templates/codex/dot_agents/skills/session_summary/SKILL.md +35 -0
- project_init/templates/codex/dot_agents/skills/start_task/SKILL.md +48 -0
- project_init/templates/codex/dot_agents/skills/status/SKILL.md +15 -0
- project_init/templates/codex/dot_codex/hooks.json.tmpl +17 -0
- project_init/templates/fallback/dot_claude/hooks/github_command_guard.sh +11 -0
- project_init/templates/fallback/dot_claude/hooks/post_edit_lint.sh +58 -0
- project_init/templates/fallback/dot_claude/hooks/pre_commit_gate.sh +81 -0
- project_init/templates/fallback/dot_claude/hooks/prod_guard.py +140 -0
- project_init/templates/fallback/dot_claude/hooks/session_setup.sh +62 -0
- project_init/templates/fallback/dot_claude/hooks/workflow_state_reminder.sh +72 -0
- project_init/templates/fallback/dot_claude/skills/INDEX.md +28 -0
- project_init/templates/fallback/dot_claude/skills/add_adr/SKILL.md +33 -0
- project_init/templates/fallback/dot_claude/skills/add_command/SKILL.md +63 -0
- project_init/templates/fallback/dot_claude/skills/add_hook/SKILL.md +112 -0
- project_init/templates/fallback/dot_claude/skills/audit/SKILL.md +146 -0
- project_init/templates/fallback/dot_claude/skills/create_issue/SKILL.md +59 -0
- project_init/templates/fallback/dot_claude/skills/github_workflow/SKILL.md +80 -0
- project_init/templates/fallback/dot_claude/skills/request_review/SKILL.md +19 -0
- project_init/templates/fallback/dot_claude/skills/review/SKILL.md +17 -0
- project_init/templates/fallback/dot_claude/skills/save_memory/SKILL.md +17 -0
- project_init/templates/fallback/dot_claude/skills/session_summary/SKILL.md +35 -0
- project_init/templates/fallback/dot_claude/skills/start_task/SKILL.md +48 -0
- project_init/templates/fallback/dot_claude/skills/status/SKILL.md +15 -0
- project_init/templates/gemini/dot_agents/skills/add_adr/SKILL.md +33 -0
- project_init/templates/gemini/dot_agents/skills/add_command/SKILL.md +63 -0
- project_init/templates/gemini/dot_agents/skills/add_hook/SKILL.md +112 -0
- project_init/templates/gemini/dot_agents/skills/audit/SKILL.md +146 -0
- project_init/templates/gemini/dot_agents/skills/create_issue/SKILL.md +59 -0
- project_init/templates/gemini/dot_agents/skills/github_workflow/SKILL.md +80 -0
- project_init/templates/gemini/dot_agents/skills/request_review/SKILL.md +19 -0
- project_init/templates/gemini/dot_agents/skills/review/SKILL.md +17 -0
- project_init/templates/gemini/dot_agents/skills/save_memory/SKILL.md +17 -0
- project_init/templates/gemini/dot_agents/skills/session_summary/SKILL.md +35 -0
- project_init/templates/gemini/dot_agents/skills/start_task/SKILL.md +48 -0
- project_init/templates/gemini/dot_agents/skills/status/SKILL.md +15 -0
- project_init/templates/gemini/dot_claude/scripts/setup_gemini.sh.tmpl +16 -0
- project_init/templates/gemini/dot_gemini-extension/commands/add_adr.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/add_command.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/add_hook.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/audit.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/create_issue.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/github_workflow.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/request_review.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/review.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/save_memory.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/session_summary.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/start_task.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/commands/status.toml +5 -0
- project_init/templates/gemini/dot_gemini-extension/gemini-extension.json.tmpl +6 -0
- project_init/templates/gemini/dot_gemini-extension/hooks/hooks.json.tmpl +18 -0
- project_init/templates/graphify/dot_claude/docs/guides/using-graphify.md +37 -0
- project_init/templates/graphify/dot_claude/rules/graphify.md +18 -0
- project_init/templates/graphify/dot_claude/scripts/setup_graphify.sh +40 -0
- project_init/templates/obsidian/dot_claude/scripts/lint_memory.sh +115 -0
- project_init/templates/obsidian/dot_claude/vault/decisions/adr-000-project-setup.md.tmpl +22 -0
- project_init/templates/obsidian/dot_claude/vault/dot_obsidian/README.md +31 -0
- project_init/templates/obsidian/dot_claude/vault/dot_obsidian/app.json +6 -0
- project_init/templates/obsidian/dot_claude/vault/dot_obsidian/community-plugins.json +1 -0
- project_init/templates/obsidian/dot_claude/vault/dot_obsidian/core-plugins.json +1 -0
- project_init/templates/obsidian/dot_claude/vault/log.md +6 -0
- project_init/templates/obsidian/dot_claude/vault/templates/decision.md +16 -0
- project_init/templates/obsidian/dot_claude/vault/templates/design-note.md +14 -0
- project_init/templates/obsidian/dot_claude/vault/templates/knowledge-note.md +12 -0
- project_init/templates/obsidian/dot_claude/vault/templates/session-note.md +16 -0
- project_init/templates/presets/obsidian-graphify.toml +16 -0
- project_init/templates/presets/obsidian-only.toml +14 -0
- project_init/upgrade.py +569 -0
- project_init-0.3.0.dist-info/METADATA +342 -0
- project_init-0.3.0.dist-info/RECORD +173 -0
- project_init-0.3.0.dist-info/WHEEL +4 -0
- project_init-0.3.0.dist-info/entry_points.txt +2 -0
- project_init-0.3.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Wait for all CI checks on a PR, then optionally merge.
|
|
3
|
+
# Only prints failures or the final pass line — no per-refresh noise.
|
|
4
|
+
# Requires: gh, python3 (stdlib only — no jq dependency).
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# .claude/scripts/monitor_pr.sh <pr-number> [--merge] [--review-cycle N] [--no-review]
|
|
8
|
+
#
|
|
9
|
+
# --merge: squash-merge and delete branch automatically when all checks pass.
|
|
10
|
+
# --review-cycle N: current review fix cycle count (0-based, default 0).
|
|
11
|
+
# When N >= MAX_REVIEW_CYCLES and review/decision is still failing or pending,
|
|
12
|
+
# force-merges with --admin.
|
|
13
|
+
# --no-review: skip all review waiting and admin-merge after CI passes.
|
|
14
|
+
# Use ONLY for solo-dev PRs where no human reviewer will ever respond.
|
|
15
|
+
# Do NOT use to avoid addressing legitimate review feedback.
|
|
16
|
+
#
|
|
17
|
+
# Full lifecycle for agents:
|
|
18
|
+
# 1. .claude/scripts/monitor_pr.sh <n> --merge
|
|
19
|
+
# 2. Exit 2 -> review comments printed -> read and address them, push
|
|
20
|
+
# 3. Re-run with --review-cycle 1
|
|
21
|
+
# 4. Exit 2 again -> address remaining comments, push
|
|
22
|
+
# 5. Re-run with --review-cycle 2 -> admin-merge fires if still blocked
|
|
23
|
+
#
|
|
24
|
+
# Review cycle policy:
|
|
25
|
+
# Two fix cycles are required before admin-merge is allowed. This ensures
|
|
26
|
+
# review feedback (including Copilot comments) is read and addressed at
|
|
27
|
+
# least once before force-merging.
|
|
28
|
+
|
|
29
|
+
set -euo pipefail
|
|
30
|
+
|
|
31
|
+
PR_NUMBER="${1:-}"
|
|
32
|
+
MODE="${2:-}"
|
|
33
|
+
REVIEW_CYCLE=0
|
|
34
|
+
MAX_REVIEW_CYCLES=2
|
|
35
|
+
NO_REVIEW=0
|
|
36
|
+
|
|
37
|
+
if [ -z "$PR_NUMBER" ]; then
|
|
38
|
+
echo "Usage: monitor_pr.sh <pr-number> [--merge] [--review-cycle N] [--no-review]" >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
if [ -n "$MODE" ] && [ "$MODE" != "--merge" ]; then
|
|
43
|
+
echo "Unknown option: $MODE" >&2
|
|
44
|
+
exit 2
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Parse remaining flags (order-independent after position 2).
|
|
48
|
+
# Shift past <pr-number> and optional --merge; remaining args are flags.
|
|
49
|
+
shift 1 # drop PR_NUMBER
|
|
50
|
+
[ "$MODE" = "--merge" ] && shift 1 # drop --merge if present
|
|
51
|
+
while [ $# -gt 0 ]; do
|
|
52
|
+
case "$1" in
|
|
53
|
+
--review-cycle) REVIEW_CYCLE="${2:-0}"; shift 2 ;;
|
|
54
|
+
--review-cycle=*) REVIEW_CYCLE="${1#*=}"; shift ;;
|
|
55
|
+
--no-review) NO_REVIEW=1; shift ;;
|
|
56
|
+
*) echo "Unknown option: $1" >&2; exit 2 ;;
|
|
57
|
+
esac
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
_count_pending() {
|
|
61
|
+
echo "$1" | python3 -c "
|
|
62
|
+
import json, sys
|
|
63
|
+
data = json.load(sys.stdin)
|
|
64
|
+
# Exclude review/decision; it is a derived commit status that only appears
|
|
65
|
+
# after a review event. We detect review state directly via reviewDecision.
|
|
66
|
+
print(sum(1 for c in data if c.get('name') != 'review/decision' and c.get('state') in ('PENDING', 'IN_PROGRESS', 'EXPECTED')))
|
|
67
|
+
"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_print_failures() {
|
|
71
|
+
echo "$1" | python3 -c "
|
|
72
|
+
import json, sys
|
|
73
|
+
data = json.load(sys.stdin)
|
|
74
|
+
bad = [
|
|
75
|
+
c for c in data
|
|
76
|
+
if c.get('name') != 'review/decision'
|
|
77
|
+
and (
|
|
78
|
+
c.get('bucket') in ('fail', 'cancel')
|
|
79
|
+
or c.get('state') in ('FAILURE', 'CANCELLED', 'TIMED_OUT', 'ERROR')
|
|
80
|
+
)
|
|
81
|
+
]
|
|
82
|
+
for c in bad:
|
|
83
|
+
print(f\" {c['name']}: {c.get('state') or c.get('bucket')}\")
|
|
84
|
+
sys.exit(len(bad))
|
|
85
|
+
"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Print review feedback — inline comments first, falls back to full PR comments view.
|
|
89
|
+
_print_review_comments() {
|
|
90
|
+
local inline
|
|
91
|
+
inline=$(
|
|
92
|
+
gh api "repos/{owner}/{repo}/pulls/$PR_NUMBER/comments" \
|
|
93
|
+
--jq '.[] | " \(.path):\(.line // "?") [\(.user.login)]\n \(.body)\n"' \
|
|
94
|
+
2>/dev/null || true
|
|
95
|
+
)
|
|
96
|
+
if [ -n "$inline" ]; then
|
|
97
|
+
printf '%s\n' "$inline"
|
|
98
|
+
else
|
|
99
|
+
gh pr view "$PR_NUMBER" --comments 2>/dev/null || true
|
|
100
|
+
fi
|
|
101
|
+
echo " Full PR: $(gh pr view "$PR_NUMBER" --json url -q '.url' 2>/dev/null || true)"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_run_gh() {
|
|
105
|
+
local output
|
|
106
|
+
local status
|
|
107
|
+
|
|
108
|
+
set +e
|
|
109
|
+
output=$(GH_PROMPT_DISABLED=1 gh "$@" 2>&1)
|
|
110
|
+
status=$?
|
|
111
|
+
set -e
|
|
112
|
+
|
|
113
|
+
if [ -n "$output" ]; then
|
|
114
|
+
printf '%s\n' "$output" | grep -v "^$" || true
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
return "$status"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_admin_merge() {
|
|
121
|
+
if _run_gh pr merge "$PR_NUMBER" --squash --delete-branch --admin; then
|
|
122
|
+
echo "Merged PR #$PR_NUMBER (admin)"
|
|
123
|
+
else
|
|
124
|
+
echo "ERROR: admin merge failed for PR #$PR_NUMBER" >&2
|
|
125
|
+
return 1
|
|
126
|
+
fi
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Query the PR's aggregate review decision directly — source of truth regardless
|
|
130
|
+
# of whether the review/decision commit status has been posted yet.
|
|
131
|
+
# Returns: APPROVED | CHANGES_REQUESTED | REVIEW_REQUIRED | (empty = no review policy)
|
|
132
|
+
# Returns UNKNOWN on API failure — callers must treat this as fail-closed.
|
|
133
|
+
_get_review_decision() {
|
|
134
|
+
gh pr view "$PR_NUMBER" --json reviewDecision -q '.reviewDecision // ""' 2>/dev/null || echo "UNKNOWN"
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Check if any review activity exists (COMMENTED, APPROVED, or CHANGES_REQUESTED).
|
|
138
|
+
# Bot reviewers like Codex post COMMENTED reviews that don't change reviewDecision,
|
|
139
|
+
# so we use this as an early exit signal from the wait loop.
|
|
140
|
+
_has_review_activity() {
|
|
141
|
+
local count
|
|
142
|
+
count=$(gh pr view "$PR_NUMBER" --json reviews -q '.reviews | length' 2>/dev/null) || count=0
|
|
143
|
+
[ "$count" -gt 0 ]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# --- Wait for all CI checks (excludes review/decision commit status) ---
|
|
147
|
+
# Guard: if checks haven't registered yet (empty list), keep polling.
|
|
148
|
+
# An empty list is indistinguishable from "all done" without this guard,
|
|
149
|
+
# which caused premature merges before CI even started.
|
|
150
|
+
while true; do
|
|
151
|
+
CHECKS=$(gh pr checks "$PR_NUMBER" --json name,state,bucket 2>/dev/null) || CHECKS="[]"
|
|
152
|
+
CHECK_COUNT=$(echo "$CHECKS" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))")
|
|
153
|
+
[ "$CHECK_COUNT" -eq 0 ] && { sleep 10; continue; }
|
|
154
|
+
PENDING=$(_count_pending "$CHECKS")
|
|
155
|
+
[ "$PENDING" -eq 0 ] && break
|
|
156
|
+
sleep 10
|
|
157
|
+
done
|
|
158
|
+
|
|
159
|
+
FAIL_CODE=0
|
|
160
|
+
_print_failures "$CHECKS" || FAIL_CODE=$?
|
|
161
|
+
|
|
162
|
+
if [ "$FAIL_CODE" -gt 0 ]; then
|
|
163
|
+
echo "CI failed on PR #$PR_NUMBER — fix the issues, push, then re-run this script."
|
|
164
|
+
exit 1
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
# --no-review: explicit bypass — skip review gate entirely after CI passes.
|
|
168
|
+
# Use only for solo-dev PRs where no reviewer will ever respond.
|
|
169
|
+
if [ "$NO_REVIEW" -eq 1 ] && [ "$MODE" = "--merge" ]; then
|
|
170
|
+
echo "PR #$PR_NUMBER: CI passed. --no-review specified — skipping review gate."
|
|
171
|
+
_admin_merge; exit 0
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
# --- Wait up to 6 min for a reviewer to act ---
|
|
175
|
+
# Query reviewDecision directly so this works before review-status.yml creates
|
|
176
|
+
# the derived review/decision commit status.
|
|
177
|
+
REVIEW_TIMEOUT=360
|
|
178
|
+
REVIEW_ELAPSED=0
|
|
179
|
+
REVIEW_DECISION=$(_get_review_decision)
|
|
180
|
+
if [ "$MODE" = "--merge" ] && [ "$REVIEW_CYCLE" -ge "$MAX_REVIEW_CYCLES" ] && [ "$REVIEW_DECISION" = "REVIEW_REQUIRED" ]; then
|
|
181
|
+
echo "Max review cycles ($MAX_REVIEW_CYCLES) reached — skipping reviewer wait and force-merging with admin override."
|
|
182
|
+
_admin_merge; exit 0
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
if [ "$REVIEW_DECISION" = "REVIEW_REQUIRED" ] || [ "$REVIEW_DECISION" = "UNKNOWN" ]; then
|
|
186
|
+
echo "Waiting for reviewer (up to ${REVIEW_TIMEOUT}s, polling every 30s) — reviewDecision: ${REVIEW_DECISION}"
|
|
187
|
+
fi
|
|
188
|
+
while { [ "$REVIEW_DECISION" = "REVIEW_REQUIRED" ] || [ "$REVIEW_DECISION" = "UNKNOWN" ]; } && [ "$REVIEW_ELAPSED" -lt "$REVIEW_TIMEOUT" ]; do
|
|
189
|
+
sleep 30
|
|
190
|
+
REVIEW_ELAPSED=$((REVIEW_ELAPSED + 30))
|
|
191
|
+
REVIEW_DECISION=$(_get_review_decision)
|
|
192
|
+
echo " [${REVIEW_ELAPSED}s/${REVIEW_TIMEOUT}s] reviewDecision: ${REVIEW_DECISION:-none}"
|
|
193
|
+
# Early exit: if any review activity exists (even COMMENTED), stop waiting.
|
|
194
|
+
# Bot reviewers like Codex post comments without changing reviewDecision.
|
|
195
|
+
if [ "$REVIEW_DECISION" = "REVIEW_REQUIRED" ] && _has_review_activity; then
|
|
196
|
+
echo " Review comments detected — proceeding without waiting for formal approval."
|
|
197
|
+
break
|
|
198
|
+
fi
|
|
199
|
+
CHECKS=$(gh pr checks "$PR_NUMBER" --json name,state,bucket 2>/dev/null) || CHECKS="[]"
|
|
200
|
+
done
|
|
201
|
+
|
|
202
|
+
if [ "$REVIEW_DECISION" = "UNKNOWN" ]; then
|
|
203
|
+
echo "ERROR: could not fetch reviewDecision for PR #$PR_NUMBER — cannot verify review state." >&2
|
|
204
|
+
exit 2
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
if [ "$REVIEW_DECISION" = "CHANGES_REQUESTED" ]; then
|
|
208
|
+
echo "Review/decision failed on PR #$PR_NUMBER (cycle $REVIEW_CYCLE/$MAX_REVIEW_CYCLES):"
|
|
209
|
+
_print_review_comments
|
|
210
|
+
|
|
211
|
+
if [ "$MODE" = "--merge" ]; then
|
|
212
|
+
if [ "$REVIEW_CYCLE" -ge "$MAX_REVIEW_CYCLES" ]; then
|
|
213
|
+
echo "Max review cycles ($MAX_REVIEW_CYCLES) reached — force-merging with admin override."
|
|
214
|
+
_admin_merge; exit 0
|
|
215
|
+
else
|
|
216
|
+
NEXT=$((REVIEW_CYCLE + 1))
|
|
217
|
+
echo "Address the comments above, push your changes, then re-run:"
|
|
218
|
+
echo " .claude/scripts/monitor_pr.sh $PR_NUMBER --merge --review-cycle $NEXT"
|
|
219
|
+
exit 2
|
|
220
|
+
fi
|
|
221
|
+
fi
|
|
222
|
+
exit 1
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
if [ "$REVIEW_DECISION" = "REVIEW_REQUIRED" ]; then
|
|
226
|
+
echo "PR #$PR_NUMBER: review/decision still pending after ${REVIEW_TIMEOUT}s — no reviewer has acted."
|
|
227
|
+
echo " Full PR: $(gh pr view "$PR_NUMBER" --json url -q '.url' 2>/dev/null || true)"
|
|
228
|
+
|
|
229
|
+
if [ "$MODE" = "--merge" ]; then
|
|
230
|
+
if [ "$REVIEW_CYCLE" -ge "$MAX_REVIEW_CYCLES" ]; then
|
|
231
|
+
echo "Max review cycles ($MAX_REVIEW_CYCLES) reached — force-merging with admin override."
|
|
232
|
+
_admin_merge; exit 0
|
|
233
|
+
else
|
|
234
|
+
NEXT=$((REVIEW_CYCLE + 1))
|
|
235
|
+
echo "Request a review or wait for a reviewer, then re-run:"
|
|
236
|
+
echo " .claude/scripts/monitor_pr.sh $PR_NUMBER --merge --review-cycle $NEXT"
|
|
237
|
+
exit 2
|
|
238
|
+
fi
|
|
239
|
+
fi
|
|
240
|
+
exit 1
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
PR_URL=$(gh pr view "$PR_NUMBER" --json url -q '.url')
|
|
244
|
+
echo "PR #$PR_NUMBER passed: $PR_URL"
|
|
245
|
+
|
|
246
|
+
if [ "$MODE" = "--merge" ]; then
|
|
247
|
+
MERGE_STATE=$(gh pr view "$PR_NUMBER" --json mergeStateStatus -q '.mergeStateStatus' 2>/dev/null || echo "UNKNOWN")
|
|
248
|
+
|
|
249
|
+
if [ "$MERGE_STATE" = "CLEAN" ] || [ "$MERGE_STATE" = "UNSTABLE" ]; then
|
|
250
|
+
if _run_gh pr merge "$PR_NUMBER" --squash --delete-branch; then
|
|
251
|
+
echo "Merged PR #$PR_NUMBER"
|
|
252
|
+
else
|
|
253
|
+
echo "ERROR: merge failed for PR #$PR_NUMBER" >&2
|
|
254
|
+
exit 1
|
|
255
|
+
fi
|
|
256
|
+
elif [ "$MERGE_STATE" = "BLOCKED" ]; then
|
|
257
|
+
echo "PR is blocked by review protection — merging with admin override."
|
|
258
|
+
_admin_merge
|
|
259
|
+
else
|
|
260
|
+
if ! _run_gh pr merge "$PR_NUMBER" --squash --delete-branch; then
|
|
261
|
+
if ! _run_gh pr merge "$PR_NUMBER" --squash --delete-branch --auto; then
|
|
262
|
+
echo "ERROR: could not merge or enable auto-merge for PR #$PR_NUMBER" >&2
|
|
263
|
+
exit 1
|
|
264
|
+
fi
|
|
265
|
+
echo "Auto-merge enabled for PR #$PR_NUMBER — will merge once all requirements are met."
|
|
266
|
+
else
|
|
267
|
+
echo "Merged PR #$PR_NUMBER"
|
|
268
|
+
fi
|
|
269
|
+
fi
|
|
270
|
+
fi
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# push_wiki.sh — clone the GitHub wiki, update Home.md, and push.
|
|
3
|
+
# Usage: push_wiki.sh <repo-slug> <wiki-source-file>
|
|
4
|
+
# repo-slug e.g. owner/repo-name
|
|
5
|
+
# wiki-source-file path to the markdown file to write as Home.md
|
|
6
|
+
#
|
|
7
|
+
# The guard allowlists this script so it is not blocked by the git-push rule.
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
if [[ $# -lt 2 ]]; then
|
|
12
|
+
echo "Usage: push_wiki.sh <repo-slug> <wiki-source-file>" >&2
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
REPO_SLUG="$1"
|
|
17
|
+
SOURCE_FILE="$2"
|
|
18
|
+
WIKI_DIR="$(mktemp -d)"
|
|
19
|
+
trap 'rm -rf "$WIKI_DIR"' EXIT
|
|
20
|
+
|
|
21
|
+
echo "Cloning wiki for $REPO_SLUG..."
|
|
22
|
+
git clone "https://github.com/${REPO_SLUG}.wiki.git" "$WIKI_DIR"
|
|
23
|
+
|
|
24
|
+
cp "$SOURCE_FILE" "$WIKI_DIR/Home.md"
|
|
25
|
+
|
|
26
|
+
cd "$WIKI_DIR"
|
|
27
|
+
git add Home.md
|
|
28
|
+
if git diff --cached --quiet; then
|
|
29
|
+
echo "Wiki already up to date."
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
git commit -m "Update Home page"
|
|
33
|
+
git push
|
|
34
|
+
echo "Wiki updated."
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Configure or check GitHub repository governance for the scaffolded workflow.
|
|
3
|
+
#
|
|
4
|
+
# Usage: setup_github.sh [branch] [--protect]
|
|
5
|
+
# --protect apply baseline branch protection to the default branch
|
|
6
|
+
# (require CI green, require PR review, block force-push).
|
|
7
|
+
# Idempotent: the PUT endpoint replaces the existing config.
|
|
8
|
+
#
|
|
9
|
+
# Requires: gh and admin permission on the repository.
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
BRANCH="main"
|
|
14
|
+
PROTECT=0
|
|
15
|
+
for arg in "$@"; do
|
|
16
|
+
case "$arg" in
|
|
17
|
+
--protect) PROTECT=1 ;;
|
|
18
|
+
--*) echo "Unknown option: $arg" >&2; exit 1 ;;
|
|
19
|
+
*) BRANCH="$arg" ;;
|
|
20
|
+
esac
|
|
21
|
+
done
|
|
22
|
+
|
|
23
|
+
if ! gh auth status >/dev/null 2>&1; then
|
|
24
|
+
echo "ERROR: gh is not authenticated. Run gh auth login first." >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
|
|
29
|
+
OWNER=${REPO%/*}
|
|
30
|
+
NAME=${REPO#*/}
|
|
31
|
+
|
|
32
|
+
echo "Configuring GitHub governance for $REPO ($BRANCH)"
|
|
33
|
+
# Default endpoint: repos/$OWNER/$NAME/branches/main/protection
|
|
34
|
+
|
|
35
|
+
if [ "$PROTECT" = 1 ]; then
|
|
36
|
+
PROTECTION=$(mktemp)
|
|
37
|
+
trap 'rm -f "$PROTECTION"' EXIT
|
|
38
|
+
|
|
39
|
+
# Contexts must match the scaffolded workflows ("<workflow> / <job name>").
|
|
40
|
+
# "CI / Integration tests" is omitted on purpose — that job is documented as
|
|
41
|
+
# user-adjustable; add it here once your integration suite is stable.
|
|
42
|
+
cat > "$PROTECTION" <<'JSON'
|
|
43
|
+
{
|
|
44
|
+
"required_status_checks": {
|
|
45
|
+
"strict": true,
|
|
46
|
+
"contexts": [
|
|
47
|
+
"CI / Lint and test",
|
|
48
|
+
"CI / Secret scan (gitleaks)",
|
|
49
|
+
"Validate PR / Check PR title, branch, and linked issue",
|
|
50
|
+
"review/decision"
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
"enforce_admins": false,
|
|
54
|
+
"required_pull_request_reviews": {
|
|
55
|
+
"required_approving_review_count": 1,
|
|
56
|
+
"dismiss_stale_reviews": true,
|
|
57
|
+
"require_code_owner_reviews": false,
|
|
58
|
+
"require_last_push_approval": false
|
|
59
|
+
},
|
|
60
|
+
"restrictions": null,
|
|
61
|
+
"required_conversation_resolution": true,
|
|
62
|
+
"allow_force_pushes": false,
|
|
63
|
+
"allow_deletions": false
|
|
64
|
+
}
|
|
65
|
+
JSON
|
|
66
|
+
|
|
67
|
+
if gh api "repos/$OWNER/$NAME/branches/$BRANCH/protection" -X PUT --input "$PROTECTION" >/dev/null; then
|
|
68
|
+
echo "Branch protection applied to $BRANCH"
|
|
69
|
+
else
|
|
70
|
+
echo "WARNING: could not apply branch protection. Check admin permissions and repository plan." >&2
|
|
71
|
+
fi
|
|
72
|
+
else
|
|
73
|
+
echo "Skipping branch protection (pass --protect to apply: require CI green, require review, block force-push)"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
if gh api "repos/$OWNER/$NAME/code-review-settings" -X PUT -f copilot_code_review_enabled=true >/dev/null 2>&1; then
|
|
77
|
+
echo "Copilot code review enabled"
|
|
78
|
+
else
|
|
79
|
+
echo "WARNING: Enable Copilot code review manually if your plan supports it:" >&2
|
|
80
|
+
echo " https://github.com/$OWNER/$NAME/settings/code_review" >&2
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# --- GitHub Project board field provisioning ---
|
|
84
|
+
# Creates the single-select metadata fields used by board-automation.yml.
|
|
85
|
+
# Requires a token with 'project' scope (set PROJECT_TOKEN env var, or ensure
|
|
86
|
+
# gh auth token has project scope). Skips fields that already exist.
|
|
87
|
+
echo ""
|
|
88
|
+
echo "Provisioning GitHub Project board fields..."
|
|
89
|
+
|
|
90
|
+
PROJECT_NUMBER="${PROJECT_NUMBER:-1}"
|
|
91
|
+
|
|
92
|
+
# Use PROJECT_TOKEN if set, otherwise fall through to default gh auth
|
|
93
|
+
if [ -n "${PROJECT_TOKEN:-}" ]; then
|
|
94
|
+
export GH_TOKEN="$PROJECT_TOKEN"
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
PROJECT_DATA=$(gh api graphql -f query='
|
|
98
|
+
query($owner: String!, $number: Int!) {
|
|
99
|
+
user(login: $owner) {
|
|
100
|
+
projectV2(number: $number) {
|
|
101
|
+
id
|
|
102
|
+
fields(first: 50) { nodes { ... on ProjectV2SingleSelectField { name } } }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
organization(login: $owner) {
|
|
106
|
+
projectV2(number: $number) {
|
|
107
|
+
id
|
|
108
|
+
fields(first: 50) { nodes { ... on ProjectV2SingleSelectField { name } } }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}' -f owner="$OWNER" -F number="$PROJECT_NUMBER" 2>/dev/null || echo '{}')
|
|
112
|
+
|
|
113
|
+
PROJECT_ID=$(echo "$PROJECT_DATA" | jq -r \
|
|
114
|
+
'(.data.user.projectV2 // .data.organization.projectV2 // {}).id // empty')
|
|
115
|
+
|
|
116
|
+
if [ -z "$PROJECT_ID" ]; then
|
|
117
|
+
echo "WARNING: Project #$PROJECT_NUMBER not found for $REPO." >&2
|
|
118
|
+
echo " Ensure PROJECT_TOKEN has 'project' scope, or create these fields manually:" >&2
|
|
119
|
+
echo " • Priority — options: high, medium, low" >&2
|
|
120
|
+
echo " • Size — options: XS, S, M, L, XL" >&2
|
|
121
|
+
echo " • Agent ready — options: Yes, No" >&2
|
|
122
|
+
echo " • Confidence — options: high, medium, low, unknown" >&2
|
|
123
|
+
echo " • Type — options: feature, bug, chore, documentation, test" >&2
|
|
124
|
+
echo " Settings: https://github.com/users/$OWNER/projects/$PROJECT_NUMBER/settings/fields" >&2
|
|
125
|
+
else
|
|
126
|
+
EXISTING_FIELDS=$(echo "$PROJECT_DATA" | jq -r \
|
|
127
|
+
'((.data.user.projectV2 // .data.organization.projectV2).fields.nodes // [])[] | .name // empty')
|
|
128
|
+
|
|
129
|
+
ensure_single_select_field() {
|
|
130
|
+
local field_name="$1"
|
|
131
|
+
local mutation="$2"
|
|
132
|
+
if printf '%s\n' "$EXISTING_FIELDS" | grep -Fxq "$field_name"; then
|
|
133
|
+
echo " '$field_name' already exists — skipping"
|
|
134
|
+
return 0
|
|
135
|
+
fi
|
|
136
|
+
if gh api graphql -f query="$mutation" -f projectId="$PROJECT_ID" >/dev/null 2>&1; then
|
|
137
|
+
echo " Created '$field_name'"
|
|
138
|
+
else
|
|
139
|
+
# Repo: $REPO Project: #$PROJECT_NUMBER
|
|
140
|
+
echo " WARNING: could not create '$field_name' for $REPO — add it manually:" >&2
|
|
141
|
+
echo " https://github.com/users/$OWNER/projects/$PROJECT_NUMBER/settings/fields" >&2
|
|
142
|
+
fi
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
ensure_single_select_field "Priority" '
|
|
146
|
+
mutation($projectId: ID!) {
|
|
147
|
+
createProjectV2Field(input: {
|
|
148
|
+
projectId: $projectId
|
|
149
|
+
dataType: SINGLE_SELECT
|
|
150
|
+
name: "Priority"
|
|
151
|
+
singleSelectOptions: [
|
|
152
|
+
{ name: "high", color: RED, description: "" }
|
|
153
|
+
{ name: "medium", color: YELLOW, description: "" }
|
|
154
|
+
{ name: "low", color: GRAY, description: "" }
|
|
155
|
+
]
|
|
156
|
+
}) { projectV2Field { ... on ProjectV2SingleSelectField { id } } }
|
|
157
|
+
}'
|
|
158
|
+
|
|
159
|
+
ensure_single_select_field "Size" '
|
|
160
|
+
mutation($projectId: ID!) {
|
|
161
|
+
createProjectV2Field(input: {
|
|
162
|
+
projectId: $projectId
|
|
163
|
+
dataType: SINGLE_SELECT
|
|
164
|
+
name: "Size"
|
|
165
|
+
singleSelectOptions: [
|
|
166
|
+
{ name: "XS", color: BLUE, description: "" }
|
|
167
|
+
{ name: "S", color: GREEN, description: "" }
|
|
168
|
+
{ name: "M", color: YELLOW, description: "" }
|
|
169
|
+
{ name: "L", color: ORANGE, description: "" }
|
|
170
|
+
{ name: "XL", color: RED, description: "" }
|
|
171
|
+
]
|
|
172
|
+
}) { projectV2Field { ... on ProjectV2SingleSelectField { id } } }
|
|
173
|
+
}'
|
|
174
|
+
|
|
175
|
+
ensure_single_select_field "Agent ready" '
|
|
176
|
+
mutation($projectId: ID!) {
|
|
177
|
+
createProjectV2Field(input: {
|
|
178
|
+
projectId: $projectId
|
|
179
|
+
dataType: SINGLE_SELECT
|
|
180
|
+
name: "Agent ready"
|
|
181
|
+
singleSelectOptions: [
|
|
182
|
+
{ name: "Yes", color: GREEN, description: "" }
|
|
183
|
+
{ name: "No", color: GRAY, description: "" }
|
|
184
|
+
]
|
|
185
|
+
}) { projectV2Field { ... on ProjectV2SingleSelectField { id } } }
|
|
186
|
+
}'
|
|
187
|
+
|
|
188
|
+
ensure_single_select_field "Confidence" '
|
|
189
|
+
mutation($projectId: ID!) {
|
|
190
|
+
createProjectV2Field(input: {
|
|
191
|
+
projectId: $projectId
|
|
192
|
+
dataType: SINGLE_SELECT
|
|
193
|
+
name: "Confidence"
|
|
194
|
+
singleSelectOptions: [
|
|
195
|
+
{ name: "high", color: GREEN, description: "" }
|
|
196
|
+
{ name: "medium", color: YELLOW, description: "" }
|
|
197
|
+
{ name: "low", color: ORANGE, description: "" }
|
|
198
|
+
{ name: "unknown", color: GRAY, description: "" }
|
|
199
|
+
]
|
|
200
|
+
}) { projectV2Field { ... on ProjectV2SingleSelectField { id } } }
|
|
201
|
+
}'
|
|
202
|
+
|
|
203
|
+
ensure_single_select_field "Type" '
|
|
204
|
+
mutation($projectId: ID!) {
|
|
205
|
+
createProjectV2Field(input: {
|
|
206
|
+
projectId: $projectId
|
|
207
|
+
dataType: SINGLE_SELECT
|
|
208
|
+
name: "Type"
|
|
209
|
+
singleSelectOptions: [
|
|
210
|
+
{ name: "feature", color: BLUE, description: "" }
|
|
211
|
+
{ name: "bug", color: RED, description: "" }
|
|
212
|
+
{ name: "chore", color: GRAY, description: "" }
|
|
213
|
+
{ name: "documentation", color: PURPLE, description: "" }
|
|
214
|
+
{ name: "test", color: YELLOW, description: "" }
|
|
215
|
+
]
|
|
216
|
+
}) { projectV2Field { ... on ProjectV2SingleSelectField { id } } }
|
|
217
|
+
}'
|
|
218
|
+
fi
|
|
219
|
+
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Start work on a GitHub issue: create branch, push, and open a draft PR.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# .claude/scripts/start_issue.sh <issue-number> <type>
|
|
6
|
+
#
|
|
7
|
+
# Types: feat fix chore docs test
|
|
8
|
+
#
|
|
9
|
+
# Composable with create_issue.sh:
|
|
10
|
+
# .claude/scripts/create_issue.sh feat "Add OAuth login" | xargs -I{} .claude/scripts/start_issue.sh {} feat
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
VALID_TYPES="feat fix chore docs test"
|
|
15
|
+
|
|
16
|
+
usage() {
|
|
17
|
+
echo "Usage: start_issue.sh <issue-number> <type>"
|
|
18
|
+
echo ""
|
|
19
|
+
echo "Types: $VALID_TYPES"
|
|
20
|
+
echo ""
|
|
21
|
+
echo "Examples:"
|
|
22
|
+
echo " start_issue.sh 42 feat"
|
|
23
|
+
echo " start_issue.sh 99 fix"
|
|
24
|
+
exit 1
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# --- Validate args ---
|
|
28
|
+
if [ $# -lt 2 ]; then
|
|
29
|
+
usage
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
ISSUE_NUMBER="$1"
|
|
33
|
+
TYPE="$2"
|
|
34
|
+
|
|
35
|
+
if ! echo "$VALID_TYPES" | grep -qw "$TYPE"; then
|
|
36
|
+
echo "ERROR: invalid type '$TYPE'. Valid types: $VALID_TYPES" >&2
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if ! [[ "$ISSUE_NUMBER" =~ ^[0-9]+$ ]]; then
|
|
41
|
+
echo "ERROR: issue number must be numeric, got '$ISSUE_NUMBER'" >&2
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# --- Resolve project key / abbreviation ---
|
|
46
|
+
# Set PROJECT_KEY env var, add `project_key: PI` to .claude/config.yaml,
|
|
47
|
+
# or let the script derive one from the repository directory name.
|
|
48
|
+
derive_project_key() {
|
|
49
|
+
if [ -n "${PROJECT_KEY:-}" ]; then
|
|
50
|
+
echo "$PROJECT_KEY"
|
|
51
|
+
return
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
local configured=""
|
|
55
|
+
configured=$(grep '^[[:space:]]*project_key:' .claude/config.yaml 2>/dev/null \
|
|
56
|
+
| head -n 1 \
|
|
57
|
+
| cut -d: -f2- \
|
|
58
|
+
| sed 's/#.*$//' \
|
|
59
|
+
| tr -d '[:space:]"' \
|
|
60
|
+
| tr -d "'" || true)
|
|
61
|
+
if [ -n "$configured" ]; then
|
|
62
|
+
echo "$configured"
|
|
63
|
+
return
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
local repo_name=""
|
|
67
|
+
repo_name=$(basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")
|
|
68
|
+
echo "$repo_name" \
|
|
69
|
+
| tr '[:lower:]' '[:upper:]' \
|
|
70
|
+
| tr -cs 'A-Z0-9' '\n' \
|
|
71
|
+
| awk 'NF { printf substr($0, 1, 1) }' \
|
|
72
|
+
| cut -c1-10
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
PROJECT_KEY=$(derive_project_key)
|
|
76
|
+
PROJECT_KEY=$(echo "$PROJECT_KEY" | tr '[:lower:]' '[:upper:]' | tr -cd 'A-Z0-9')
|
|
77
|
+
if [ -z "$PROJECT_KEY" ]; then
|
|
78
|
+
PROJECT_KEY="PROJ"
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# --- Fetch issue title ---
|
|
82
|
+
ISSUE_TITLE=$(gh issue view "$ISSUE_NUMBER" --json title -q '.title' 2>/dev/null)
|
|
83
|
+
if [ -z "$ISSUE_TITLE" ]; then
|
|
84
|
+
echo "ERROR: issue #$ISSUE_NUMBER not found" >&2
|
|
85
|
+
exit 1
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
ISSUE_REF="${PROJECT_KEY}-${ISSUE_NUMBER}"
|
|
89
|
+
|
|
90
|
+
# --- Derive branch name: <issue_type>/<project_abbr>-<issue_number>-<kebab-slug>, max 80 chars total ---
|
|
91
|
+
# Matches convention: feat/PI-42-add-oauth-login, fix/API-99-null-pointer
|
|
92
|
+
# Strip leading [type] prefix from issue title if present (e.g. "[feat] Add OAuth" -> "Add OAuth")
|
|
93
|
+
CLEAN_TITLE=$(echo "$ISSUE_TITLE" | sed 's/^\[[^]]*\] *//')
|
|
94
|
+
SLUG=$(echo "$CLEAN_TITLE" \
|
|
95
|
+
| tr '[:upper:]' '[:lower:]' \
|
|
96
|
+
| tr -cs 'a-z0-9' '-' \
|
|
97
|
+
| sed 's/^-//;s/-$//')
|
|
98
|
+
PREFIX="${ISSUE_REF}-"
|
|
99
|
+
MAX_SLUG=$(( 80 - ${#TYPE} - 1 - ${#PREFIX} )) # -1 for the /
|
|
100
|
+
if [ "$MAX_SLUG" -lt 12 ]; then
|
|
101
|
+
MAX_SLUG=12
|
|
102
|
+
fi
|
|
103
|
+
SLUG="${SLUG:0:$MAX_SLUG}"
|
|
104
|
+
SLUG="${SLUG%-}" # trim trailing dash if truncated mid-word
|
|
105
|
+
BRANCH="${TYPE}/${PREFIX}${SLUG}"
|
|
106
|
+
|
|
107
|
+
echo "Branch: $BRANCH"
|
|
108
|
+
|
|
109
|
+
# --- Guard: already on this branch or it already exists ---
|
|
110
|
+
CURRENT=$(git branch --show-current)
|
|
111
|
+
if [ "$CURRENT" = "$BRANCH" ]; then
|
|
112
|
+
echo "Already on branch $BRANCH"
|
|
113
|
+
elif git show-ref --verify --quiet "refs/heads/$BRANCH"; then
|
|
114
|
+
echo "Branch $BRANCH already exists - switching"
|
|
115
|
+
git checkout "$BRANCH"
|
|
116
|
+
else
|
|
117
|
+
git checkout -b "$BRANCH"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# --- Push and set upstream (retry + remote-SHA verification) ---
|
|
121
|
+
.claude/scripts/push_branch.sh "$BRANCH"
|
|
122
|
+
|
|
123
|
+
# --- Open draft PR ---
|
|
124
|
+
# Conventional Commits with issue key as scope (ADR-006)
|
|
125
|
+
PR_TITLE="${TYPE}(${ISSUE_REF}): ${CLEAN_TITLE}"
|
|
126
|
+
PR_BODY="Closes #${ISSUE_NUMBER}"
|
|
127
|
+
|
|
128
|
+
PR_URL=$(gh pr create \
|
|
129
|
+
--draft \
|
|
130
|
+
--title "$PR_TITLE" \
|
|
131
|
+
--body "$PR_BODY")
|
|
132
|
+
|
|
133
|
+
echo "Draft PR: $PR_URL"
|
|
134
|
+
|