@jeiemgi/cckit 0.1.6

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.
Files changed (191) hide show
  1. package/.claude-plugin/plugin.json +22 -0
  2. package/AGENTS.md +101 -0
  3. package/LICENSE-APACHE +202 -0
  4. package/LICENSE-MIT +21 -0
  5. package/README.md +143 -0
  6. package/SECURITY.md +22 -0
  7. package/bin/cckit +215 -0
  8. package/cckit.config.json +34 -0
  9. package/commands/kit-add.md +42 -0
  10. package/commands/kit-docs.md +45 -0
  11. package/commands/kit-doctor.md +52 -0
  12. package/commands/kit-export-project.md +58 -0
  13. package/commands/kit-export-training.md +49 -0
  14. package/commands/kit-init.md +126 -0
  15. package/commands/kit-routines.md +59 -0
  16. package/commands/kit-update.md +132 -0
  17. package/docs/kit-annotate/01-explainer.html +225 -0
  18. package/docs/kit-annotate/02-implementation-plan.html +196 -0
  19. package/docs/media/.onboarding-capture.cast +5 -0
  20. package/docs/media/README.md +43 -0
  21. package/docs/media/build-demo.sh +63 -0
  22. package/docs/media/build-kit-init.sh +51 -0
  23. package/docs/media/build-onboarding.sh +51 -0
  24. package/docs/media/kit-dry-run.cast +107 -0
  25. package/docs/media/kit-dry-run.gif +0 -0
  26. package/docs/media/kit-init.cast +56 -0
  27. package/docs/media/kit-init.gif +0 -0
  28. package/docs/media/kit-onboarding.cast +148 -0
  29. package/docs/media/kit-onboarding.gif +0 -0
  30. package/githooks/pre-commit +18 -0
  31. package/kit.config.schema.json +105 -0
  32. package/package.json +54 -0
  33. package/privacy-denylist.example +8 -0
  34. package/profiles/automation.json +36 -0
  35. package/profiles/content.json +41 -0
  36. package/profiles/minimal.json +31 -0
  37. package/profiles/research.json +37 -0
  38. package/profiles/software.json +32 -0
  39. package/scripts/annotate-setup.sh +149 -0
  40. package/scripts/autopilot.sh +50 -0
  41. package/scripts/capture-project-ids.sh +53 -0
  42. package/scripts/check.sh +66 -0
  43. package/scripts/contribute.sh +48 -0
  44. package/scripts/debug.sh +54 -0
  45. package/scripts/init-upgrade-test.sh +99 -0
  46. package/scripts/init.sh +827 -0
  47. package/scripts/install.sh +24 -0
  48. package/scripts/kit-add-test.sh +62 -0
  49. package/scripts/kit-add.sh +115 -0
  50. package/scripts/kit-adopt-test.sh +61 -0
  51. package/scripts/kit-adopt.sh +122 -0
  52. package/scripts/kit-bump-version.sh +79 -0
  53. package/scripts/kit-digest.sh +126 -0
  54. package/scripts/kit-doctor.sh +663 -0
  55. package/scripts/kit-export-project-test.sh +82 -0
  56. package/scripts/kit-export-project.sh +245 -0
  57. package/scripts/kit-export-training-test.sh +51 -0
  58. package/scripts/kit-export-training.sh +175 -0
  59. package/scripts/kit-migrate-test.sh +80 -0
  60. package/scripts/kit-migrate.sh +190 -0
  61. package/scripts/kit-onboard-test.sh +63 -0
  62. package/scripts/kit-onboard.sh +69 -0
  63. package/scripts/kit-promote-test.sh +54 -0
  64. package/scripts/kit-promote.sh +102 -0
  65. package/scripts/kit-remove-test.sh +61 -0
  66. package/scripts/kit-remove.sh +84 -0
  67. package/scripts/kit-routines.sh +322 -0
  68. package/scripts/kit-version-check.sh +91 -0
  69. package/scripts/kit-wire-test.sh +54 -0
  70. package/scripts/kit-wire.sh +132 -0
  71. package/scripts/knowledge-lint.sh +96 -0
  72. package/scripts/lib/cckit-output.sh +36 -0
  73. package/scripts/lib/effort-metrics.sh +452 -0
  74. package/scripts/lib/effort-ops-test.sh +83 -0
  75. package/scripts/lib/effort-ops.sh +132 -0
  76. package/scripts/lib/effort-plan.sh +104 -0
  77. package/scripts/lib/effort.sh +191 -0
  78. package/scripts/lib/engine-adapter.sh +92 -0
  79. package/scripts/lib/gh-log.sh +58 -0
  80. package/scripts/lib/gh-project.sh +212 -0
  81. package/scripts/lib/handoff.sh +35 -0
  82. package/scripts/lib/kit-cli-test.sh +42 -0
  83. package/scripts/lib/kit-cli.sh +32 -0
  84. package/scripts/lib/kit-config-resolve.sh +145 -0
  85. package/scripts/lib/kit-config.sh +88 -0
  86. package/scripts/lib/kit-engine-test.sh +107 -0
  87. package/scripts/lib/kit-events.sh +62 -0
  88. package/scripts/lib/kit-gc.sh +117 -0
  89. package/scripts/lib/kit-interview-test.sh +77 -0
  90. package/scripts/lib/kit-interview.sh +203 -0
  91. package/scripts/lib/kit-local.sh +79 -0
  92. package/scripts/lib/kit-manifest.sh +127 -0
  93. package/scripts/lib/kit-mode-test.sh +49 -0
  94. package/scripts/lib/kit-mode.sh +67 -0
  95. package/scripts/lib/kit-operate.sh +105 -0
  96. package/scripts/lib/kit-profile-test.sh +62 -0
  97. package/scripts/lib/kit-profile.sh +115 -0
  98. package/scripts/lib/kit-task-ops-test.sh +63 -0
  99. package/scripts/lib/kit-task-ops.sh +341 -0
  100. package/scripts/lib/pr-evidence.sh +173 -0
  101. package/scripts/lib/project-scan.sh +16 -0
  102. package/scripts/lib/react-detect.sh +78 -0
  103. package/scripts/lib/role-identity.sh +47 -0
  104. package/scripts/lib/secret-guard.sh +96 -0
  105. package/scripts/lib/toon.sh +35 -0
  106. package/scripts/lib/ui.sh +42 -0
  107. package/scripts/lib/version-bump.sh +59 -0
  108. package/scripts/lib/worktree-issue-test.sh +45 -0
  109. package/scripts/lib/worktree-issue.sh +73 -0
  110. package/scripts/lib/worktree-start.sh +280 -0
  111. package/scripts/orchestrate.sh +160 -0
  112. package/scripts/portable-test.sh +53 -0
  113. package/scripts/publish.sh +94 -0
  114. package/scripts/setup-labels.sh +25 -0
  115. package/scripts/setup-milestones.sh +17 -0
  116. package/scripts/showcase.sh +64 -0
  117. package/scripts/status.sh +44 -0
  118. package/scripts/task-sync.sh +59 -0
  119. package/scripts/test.sh +48 -0
  120. package/scripts/web-install.sh +22 -0
  121. package/skills/kit-annotate/SKILL.md +107 -0
  122. package/skills/kit-autopilot/SKILL.md +108 -0
  123. package/skills/kit-contribute/SKILL.md +134 -0
  124. package/skills/kit-customize/SKILL.md +134 -0
  125. package/skills/kit-dev/SKILL.md +67 -0
  126. package/skills/kit-digest/SKILL.md +41 -0
  127. package/skills/kit-effort-close/SKILL.md +156 -0
  128. package/skills/kit-effort-new/SKILL.md +173 -0
  129. package/skills/kit-effort-pr/SKILL.md +139 -0
  130. package/skills/kit-effort-start/SKILL.md +85 -0
  131. package/skills/kit-gc/SKILL.md +80 -0
  132. package/skills/kit-onboard/SKILL.md +50 -0
  133. package/skills/kit-security-sweep/SKILL.md +57 -0
  134. package/skills/kit-ship/SKILL.md +43 -0
  135. package/skills/kit-task-close/SKILL.md +66 -0
  136. package/skills/kit-task-new/SKILL.md +51 -0
  137. package/skills/kit-task-pr/SKILL.md +43 -0
  138. package/skills/kit-task-pr-auto/SKILL.md +27 -0
  139. package/skills/kit-task-pr-merge/SKILL.md +53 -0
  140. package/skills/kit-task-start/SKILL.md +76 -0
  141. package/skills/kit-task-sync/SKILL.md +37 -0
  142. package/templates/CLAUDE.md.tmpl +106 -0
  143. package/templates/agents/analyst.md +55 -0
  144. package/templates/agents/auto-dev.md +93 -0
  145. package/templates/agents/backend.md +59 -0
  146. package/templates/agents/designer.md +73 -0
  147. package/templates/agents/devops.md +57 -0
  148. package/templates/agents/editor.md +48 -0
  149. package/templates/agents/frontend.md +81 -0
  150. package/templates/agents/generalist.md +46 -0
  151. package/templates/agents/local-delegate.md +70 -0
  152. package/templates/agents/n8n.md +65 -0
  153. package/templates/agents/pm.md +69 -0
  154. package/templates/agents/qa.md +66 -0
  155. package/templates/agents/researcher.md +57 -0
  156. package/templates/agents/security.md +65 -0
  157. package/templates/agents/tech-lead.md +75 -0
  158. package/templates/hooks/guard-base-branch-commit.sh.tmpl +45 -0
  159. package/templates/hooks/kit-local-status.sh.tmpl +34 -0
  160. package/templates/hooks/kit_version_check.sh.tmpl +6 -0
  161. package/templates/hooks/mempal_followup.sh.tmpl +97 -0
  162. package/templates/hooks/mempal_precompact.sh.tmpl +4 -0
  163. package/templates/hooks/mempal_save.sh.tmpl +4 -0
  164. package/templates/hooks/mempal_session_start.sh.tmpl +8 -0
  165. package/templates/hooks/prepush_gate.sh.tmpl +36 -0
  166. package/templates/hooks/repo-hygiene.sh.tmpl +72 -0
  167. package/templates/kit.config.json.tmpl +32 -0
  168. package/templates/knowledge-INDEX.md.tmpl +12 -0
  169. package/templates/lib/kit-sigil.sh.tmpl +124 -0
  170. package/templates/rules/branch-naming.md +104 -0
  171. package/templates/rules/communication-style.md +22 -0
  172. package/templates/rules/delegation-brief.md +40 -0
  173. package/templates/rules/design-routing.md +35 -0
  174. package/templates/rules/effort-model.md +122 -0
  175. package/templates/rules/knowledge-base.md +41 -0
  176. package/templates/rules/mempalace.md +110 -0
  177. package/templates/rules/plan-output-format.md +58 -0
  178. package/templates/rules/react-annotate.md +69 -0
  179. package/templates/rules/risk-tiered-review.md +62 -0
  180. package/templates/rules/skill-gaps.md +48 -0
  181. package/templates/rules/task-management.md +42 -0
  182. package/templates/settings/settings.local.json.tmpl +27 -0
  183. package/templates/skills/NAMESPACED +13 -0
  184. package/templates/skills/copywriting/SKILL.md +252 -0
  185. package/templates/skills/copywriting/references/copy-frameworks.md +344 -0
  186. package/templates/skills/copywriting/references/natural-transitions.md +272 -0
  187. package/templates/skills/feature-build-refine/SKILL.md +367 -0
  188. package/templates/skills/karpathy-guidelines/SKILL.md +69 -0
  189. package/templates/skills/morning-briefing/SKILL.md +46 -0
  190. package/templates/skills/speckit/SKILL.md +239 -0
  191. package/templates/skills/supabase-patterns/SKILL.md +88 -0
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env bash
2
+ # kit-task-ops.sh — the ONE canonical home for the kit-task pr|pr-merge|new|close
3
+ # git/GitHub mechanics (Family 1 of kit-engine-boundary.md, rule #1/#2).
4
+ #
5
+ # The four .claude/skills/kit-task-{pr,pr-merge,new,close} skills, the `scripts/kit task <op>`
6
+ # dispatcher, and any future surface CALL these functions — they never re-implement the logic.
7
+ # Each function is parameterized (no AskUserQuestion / no interactive prompts here — the caller
8
+ # collects inputs), and sources the existing helpers (role-identity, gh-project ids).
9
+ #
10
+ # Source it: source scripts/lib/kit-task-ops.sh
11
+ # Requires: git, gh, jq. bash 3.2 compatible; sourceable under zsh too (no bash-only indirection).
12
+ # Self-test: bash scripts/lib/kit-task-ops-test.sh (pure helpers only — no network)
13
+ #
14
+ # Functions:
15
+ # kto_repo echo the repo slug (owner/name)
16
+ # kto_branch_issue_number [branch] echo issue # parsed from a <kind>/<N>-<slug> branch
17
+ # kto_labels_role <labels-csv> echo the role slug from a labels CSV (role:<slug>)
18
+ # kto_labels_kind <labels-csv> echo the kind slug from a labels CSV (kind:<slug>)
19
+ # kto_compose_pr_body <num> <summary> <role-slug> echo a PR body from the template
20
+ # kto_compose_issue_body <user-body> <plan> <blocked> <role-slug> echo an issue body
21
+ # kto_closing_issue_numbers <pr-num> echo the issue #s a merged PR closes
22
+ # kto_task_pr <num> <summary> [commit-msg] commit+push+open PR (board → In Review)
23
+ # kto_task_pr_merge [pr-num] squash-merge + close issues + switch develop + GC
24
+ # kto_task_new <title> <role> <kind> <prio> <ms> [plan] [blocked] <body> create issue + board fields
25
+ # kto_task_close <num> <summary> [pr-num] close issue + board → Done
26
+
27
+ # --- repo resolution -----------------------------------------------------------------------------
28
+ # Resolve the repo slug from KIT_REPO (kit-config) when available, else the kit's home repo. A
29
+ # caller may export KIT_TASK_REPO to override.
30
+ if [[ -z "${KIT_REPO:-}" ]]; then
31
+ _kto_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
32
+ if [[ -n "$_kto_root" && -f "$_kto_root/scripts/lib/kit-config.sh" ]]; then
33
+ # shellcheck source=/dev/null
34
+ source "$_kto_root/scripts/lib/kit-config.sh" && load_kit_config >/dev/null 2>&1 || true
35
+ fi
36
+ unset _kto_root
37
+ fi
38
+
39
+ kto_repo() {
40
+ printf '%s' "${KIT_TASK_REPO:-${KIT_REPO:-jeiemgi/cckit}}"
41
+ }
42
+
43
+ # The base branch a PR targets / pr-merge returns to (portable: KIT_BASE_BRANCH, default develop).
44
+ kto_base_branch() {
45
+ printf '%s' "${KIT_BASE_BRANCH:-develop}"
46
+ }
47
+
48
+ # True (rc 0) iff board updates should run. Off when KIT_PROJECTS_V2 is explicitly "false" — keeps
49
+ # the op portable to a project with no Projects v2 board (the plugin's KIT_PROJECTS_V2 toggle).
50
+ kto_board_enabled() {
51
+ [[ "${KIT_PROJECTS_V2:-true}" != "false" ]]
52
+ }
53
+
54
+ # --- pure parsers (network-free; covered by the self-test) ---------------------------------------
55
+
56
+ # Parse the issue number from a `<kind>/<N>-<slug>` branch (e.g. feat/173-x -> 173). Empty if none.
57
+ kto_branch_issue_number() {
58
+ local branch="${1:-$(git rev-parse --abbrev-ref HEAD 2>/dev/null)}"
59
+ printf '%s' "$branch" | grep -oE '/[0-9]+-' | head -1 | tr -d '/-'
60
+ }
61
+
62
+ # Echo the role slug from a comma-separated labels list (role:<slug>). Empty if absent.
63
+ kto_labels_role() {
64
+ printf '%s' "$1" | tr ',' '\n' | grep '^role:' | head -1 | cut -d: -f2
65
+ }
66
+
67
+ # Echo the kind slug from a comma-separated labels list (kind:<slug>). Empty if absent.
68
+ kto_labels_kind() {
69
+ printf '%s' "$1" | tr ',' '\n' | grep '^kind:' | head -1 | cut -d: -f2
70
+ }
71
+
72
+ # Compose a PR body from the kit-task-pr template. Args: <num> <summary> <role-slug>
73
+ kto_compose_pr_body() {
74
+ local num="$1" summary="$2" role="$3" sig=""
75
+ if type role_signature >/dev/null 2>&1; then sig="$(role_signature "$role")"; fi
76
+ cat <<EOF
77
+ ## Closes
78
+
79
+ Closes #$num
80
+
81
+ ## Summary
82
+
83
+ $summary
84
+
85
+ ## Verification
86
+
87
+ - [ ] Reproduced manually
88
+ - [ ] No unrelated files in the diff
89
+ - [ ] Labels match the issue
90
+ - [ ] If plan deliverable: opened in browser, styles render
91
+
92
+ ## Notes for review
93
+
94
+
95
+
96
+ ## Out of scope
97
+
98
+
99
+
100
+ $sig
101
+
102
+ ---
103
+ <sub>· \`claude-kit\` · kit-task-pr</sub>
104
+ EOF
105
+ }
106
+
107
+ # Compose an issue body. Args: <user-body> <plan-link> <blocked-by> <role-slug>
108
+ kto_compose_issue_body() {
109
+ local body_user="$1" plan="$2" blocked="$3" role="$4" repo base out="" sig=""
110
+ repo="$(kto_repo)"; base="$(kto_base_branch)"
111
+ [[ -n "$plan" ]] && out+="**Plan:** [\`$plan\`](https://github.com/$repo/blob/$base/$plan)"$'\n\n'
112
+ [[ -n "$blocked" ]] && out+="**Blocked by:** $blocked"$'\n\n'
113
+ out+="$body_user"
114
+ if type role_signature >/dev/null 2>&1; then sig="$(role_signature "$role")"; fi
115
+ [[ -n "$sig" ]] && out+=$'\n\n---\n'"$sig"
116
+ printf '%s' "$out"
117
+ }
118
+
119
+ # Echo the issue numbers a merged PR closes — GitHub's parse first, regex over the body as fallback.
120
+ # Args: <pr-num>
121
+ kto_closing_issue_numbers() {
122
+ local pr="$1" repo issues body
123
+ repo="$(kto_repo)"
124
+ issues=$(gh pr view "$pr" --repo "$repo" --json closingIssuesReferences \
125
+ --jq '.closingIssuesReferences[].number' 2>/dev/null)
126
+ if [[ -z "$issues" ]]; then
127
+ body=$(gh pr view "$pr" --repo "$repo" --json body --jq .body 2>/dev/null)
128
+ issues=$(printf '%s\n' "$body" \
129
+ | grep -ioE '(close[sd]?|fix(e[sd])?|resolve[sd]?) +#[0-9]+' \
130
+ | grep -oE '[0-9]+' | sort -u)
131
+ fi
132
+ printf '%s' "$issues"
133
+ }
134
+
135
+ # --- the four ops (network; not exercised by the self-test) --------------------------------------
136
+
137
+ # Map a role slug to the board's Role single-select option id (uses gh-project's role_option_id,
138
+ # which takes a display name — translate slug -> display via role_display).
139
+ _kto_role_option_for_slug() {
140
+ local slug="$1" disp=""
141
+ if type role_display >/dev/null 2>&1; then disp="$(role_display "$slug")"; fi
142
+ [[ -z "$disp" ]] && return 0
143
+ if type role_option_id >/dev/null 2>&1; then role_option_id "$disp"; fi
144
+ }
145
+
146
+ # kit task pr — commit current changes (role identity in a Co-authored-by TRAILER, never the
147
+ # author field — Vercel rejects unknown author emails), push, open the PR with the issue's
148
+ # labels/milestone, add it to the board In Review, and move the issue to In Review.
149
+ # Args: <issue-num> <summary> [commit-msg]
150
+ kto_task_pr() {
151
+ # Default-safe reads: callers (scripts/kit) may run under `set -u`.
152
+ local num="${1:-}" summary="${2:-}" commit_msg="${3:-}"
153
+ local repo branch meta title labels milestone role author url pr_num pr_node item role_opt issue_item
154
+ repo="$(kto_repo)"
155
+ branch="$(git rev-parse --abbrev-ref HEAD)"
156
+
157
+ [[ -n "$num" ]] || num="$(kto_branch_issue_number "$branch")"
158
+ [[ -n "$num" ]] || { echo "✗ kto_task_pr: branch '$branch' doesn't match <kind>/<N>-<slug> and no issue number passed" >&2; return 1; }
159
+ [[ -n "$summary" ]] || { echo "✗ kto_task_pr: a Summary is required (never invent one)" >&2; return 1; }
160
+
161
+ meta=$(gh issue view "$num" --repo "$repo" --json title,labels,milestone) || return 1
162
+ title=$(printf '%s' "$meta" | jq -r .title)
163
+ labels=$(printf '%s' "$meta" | jq -r '[.labels[].name] | join(",")')
164
+ milestone=$(printf '%s' "$meta" | jq -r '.milestone.title // ""')
165
+ role="$(kto_labels_role "$labels")"
166
+ [[ -z "$commit_msg" ]] && commit_msg="${role:-task}: $title"
167
+
168
+ if [[ -n "$(git status --porcelain)" ]]; then
169
+ git add -A
170
+ if type role_git_author >/dev/null 2>&1; then author="$(role_git_author "$role")"; fi
171
+ if [[ -n "$author" ]]; then
172
+ git commit -m "$commit_msg" -m "Co-authored-by: ${author%%|*} <${author##*|}>"
173
+ else
174
+ git commit -m "$commit_msg"
175
+ fi
176
+ fi
177
+
178
+ git push -u origin "$branch" || return 1
179
+
180
+ url=$(gh pr create --repo "$repo" \
181
+ --base "$(kto_base_branch)" --head "$branch" \
182
+ --title "$title" \
183
+ --body "$(kto_compose_pr_body "$num" "$summary" "$role")" \
184
+ --label "$labels" \
185
+ ${milestone:+--milestone "$milestone"} \
186
+ --assignee "@me") || return 1
187
+ echo "✓ $url"
188
+
189
+ # Board: add the PR (In Review) + move the issue to In Review. Best-effort — never fail the op.
190
+ if kto_board_enabled && type load_project_ids >/dev/null 2>&1 && load_project_ids 2>/dev/null; then
191
+ pr_num="$(basename "$url")"
192
+ pr_node=$(gh api "repos/$repo/pulls/$pr_num" --jq .node_id 2>/dev/null)
193
+ if [[ -n "$pr_node" ]]; then
194
+ item=$(project_add_item "$pr_node" 2>/dev/null || true)
195
+ if [[ -n "$item" ]]; then
196
+ role_opt="$(_kto_role_option_for_slug "$role")"
197
+ [[ -n "$role_opt" ]] && project_set_single_select "$item" "$ROLE_FIELD_ID" "$role_opt" 2>/dev/null || true
198
+ [[ -n "${STATUS_OPT_IN_REVIEW:-}" ]] && project_set_single_select "$item" "$STATUS_FIELD_ID" "$STATUS_OPT_IN_REVIEW" 2>/dev/null || true
199
+ fi
200
+ fi
201
+ issue_item=$(project_find_item_by_issue "$num" 2>/dev/null || true)
202
+ [[ -n "$issue_item" && -n "${STATUS_OPT_IN_REVIEW:-}" ]] && \
203
+ project_set_single_select "$issue_item" "$STATUS_FIELD_ID" "$STATUS_OPT_IN_REVIEW" 2>/dev/null || true
204
+ fi
205
+ }
206
+
207
+ # kit task pr-merge — squash-merge the open PR for the current branch (rebase-fix conflicts onto
208
+ # develop first), close its linked issues (GitHub native auto-close only fires on main), switch the
209
+ # main checkout to develop + pull, then clean up the merged branch + worktree.
210
+ # Args: [pr-num] (defaults to the open PR for the current branch)
211
+ kto_task_pr_merge() {
212
+ local input_pr="${1:-}"
213
+ local repo branch base pr_num mergeable merge_state issues n state wt main_wt
214
+ repo="$(kto_repo)"
215
+ base="$(kto_base_branch)"
216
+ branch="$(git rev-parse --abbrev-ref HEAD)"
217
+
218
+ if [[ -n "$(git status --porcelain)" ]]; then
219
+ echo "✗ Working tree is dirty — commit or stash changes first" >&2; return 1
220
+ fi
221
+
222
+ pr_num="$input_pr"
223
+ if [[ -z "$pr_num" ]]; then
224
+ pr_num=$(gh pr list --repo "$repo" --head "$branch" --state open --json number --jq '.[0].number // empty')
225
+ [[ -z "$pr_num" ]] && { echo "✗ No open PR found for branch $branch" >&2; return 1; }
226
+ fi
227
+ echo "→ PR #$pr_num on branch $branch"
228
+
229
+ mergeable=$(gh pr view "$pr_num" --repo "$repo" --json mergeable,mergeStateStatus \
230
+ --jq '{mergeable:.mergeable, status:.mergeStateStatus}')
231
+ merge_state=$(printf '%s' "$mergeable" | jq -r .mergeable)
232
+ echo " mergeable=$merge_state status=$(printf '%s' "$mergeable" | jq -r .status)"
233
+
234
+ if [[ "$merge_state" == "CONFLICTING" ]]; then
235
+ echo "→ Conflicts detected — rebasing onto $base..."
236
+ git fetch origin "$base"
237
+ if git rebase "origin/$base"; then
238
+ git push --force-with-lease origin "$branch"
239
+ echo "✓ Rebased and pushed — re-checking mergeability..."
240
+ else
241
+ echo "✗ Rebase had conflicts that need manual resolution — resolve, then run pr-merge again" >&2
242
+ git rebase --abort 2>/dev/null || true
243
+ return 1
244
+ fi
245
+ fi
246
+
247
+ if ! gh pr merge "$pr_num" --repo "$repo" --squash 2>&1; then
248
+ echo "✗ Merge failed — check PR status: https://github.com/$repo/pull/$pr_num" >&2
249
+ return 1
250
+ fi
251
+ echo "✓ PR #$pr_num merged"
252
+
253
+ # Close linked issues (replaces issue-close-on-merge.yml; native auto-close only fires on main).
254
+ issues="$(kto_closing_issue_numbers "$pr_num")"
255
+ for n in $issues; do
256
+ state=$(gh issue view "$n" --repo "$repo" --json state --jq .state 2>/dev/null)
257
+ if [[ "$state" == "OPEN" ]]; then
258
+ gh issue close "$n" --repo "$repo" \
259
+ --comment "Closed automatically on merge of #$pr_num to $base (kit task pr-merge; GitHub native auto-close only fires on the default branch)." \
260
+ && echo "✓ Closed issue #$n"
261
+ elif [[ -n "$state" ]]; then
262
+ echo " issue #$n already $state — skipped"
263
+ fi
264
+ done
265
+
266
+ # Locate the branch's worktree + the main worktree, switch develop + pull, clean up.
267
+ wt=$(git worktree list --porcelain | awk -v b="refs/heads/$branch" '/^worktree /{p=$2} /^branch /{if($2==b) print p}')
268
+ main_wt=$(git worktree list --porcelain | awk '/^worktree /{print $2; exit}')
269
+ git -C "$main_wt" checkout "$base" 2>/dev/null || git checkout "$base"
270
+ git -C "$main_wt" pull origin "$base" 2>/dev/null || git pull origin "$base"
271
+
272
+ if [[ -n "$wt" && "$wt" != "$main_wt" ]]; then
273
+ git worktree remove "$wt" --force && echo "✓ Removed worktree $wt"
274
+ fi
275
+ git branch -D "$branch" 2>/dev/null && echo "✓ Deleted local branch $branch"
276
+ git push origin --delete "$branch" 2>/dev/null && echo "✓ Deleted remote branch $branch" || echo " (remote branch already gone)"
277
+ git worktree prune
278
+ echo "✓ On $base, up to date — merged branch + worktree cleaned"
279
+ }
280
+
281
+ # kit task new — create a GitHub issue with labels + milestone, add it to the board, set Role +
282
+ # Status(Todo) + Plan Link. Inputs are passed (the caller does the AskUserQuestion). No invented
283
+ # values; fails loudly on an unknown label family upstream (the caller validates the closed sets).
284
+ # Args: <title> <role-display-or-slug> <kind> <priority> <milestone> [plan-link] [blocked-by] <body>
285
+ kto_task_new() {
286
+ local title="${1:-}" role="${2:-}" kind="${3:-}" prio="${4:-}" milestone="${5:-}" plan="${6:-}" blocked="${7:-}" body_user="${8:-}"
287
+ local repo role_slug labels body url num node item role_opt
288
+ repo="$(kto_repo)"
289
+ [[ -n "$title" && -n "$role" && -n "$kind" && -n "$prio" && -n "$milestone" ]] || {
290
+ echo "✗ kto_task_new: title, role, kind, priority, milestone are required" >&2; return 1; }
291
+
292
+ role_slug="$(printf '%s' "$role" | tr 'A-Z ' 'a-z-')"
293
+ labels="kind:${kind},priority:${prio},role:${role_slug}"
294
+ body="$(kto_compose_issue_body "$body_user" "$plan" "$blocked" "$role_slug")"
295
+
296
+ url=$(gh issue create --repo "$repo" \
297
+ --title "$title" --body "$body" \
298
+ --label "$labels" --milestone "$milestone" --assignee "@me") || return 1
299
+ num=$(basename "$url")
300
+ echo "✓ $url"
301
+
302
+ if kto_board_enabled && type load_project_ids >/dev/null 2>&1 && load_project_ids 2>/dev/null; then
303
+ node=$(gh api "repos/$repo/issues/$num" --jq .node_id 2>/dev/null)
304
+ if [[ -n "$node" ]]; then
305
+ item=$(project_add_item "$node" 2>/dev/null || true)
306
+ if [[ -n "$item" ]]; then
307
+ role_opt="$(_kto_role_option_for_slug "$role_slug")"
308
+ [[ -n "$role_opt" ]] && project_set_single_select "$item" "$ROLE_FIELD_ID" "$role_opt" 2>/dev/null || true
309
+ [[ -n "${STATUS_OPT_TODO:-}" ]] && project_set_single_select "$item" "$STATUS_FIELD_ID" "$STATUS_OPT_TODO" 2>/dev/null || true
310
+ [[ -n "$plan" && -n "${PLAN_LINK_FIELD_ID:-}" ]] && project_set_text "$item" "$PLAN_LINK_FIELD_ID" "$plan" 2>/dev/null || true
311
+ fi
312
+ fi
313
+ fi
314
+ printf '%s' "$num"
315
+ }
316
+
317
+ # kit task close — set the board Status to Done and close the issue (with a summary comment).
318
+ # Plan-badge flip stays in the skill (it edits a repo file → needs a worktree/PR). Args: <num> <summary> [pr-num]
319
+ kto_task_close() {
320
+ local num="${1:-}" summary="${2:-}"
321
+ local repo issue_json state item
322
+ repo="$(kto_repo)"
323
+ [[ -n "$num" ]] || { echo "✗ kto_task_close: issue number required" >&2; return 1; }
324
+ [[ -n "$summary" ]] || { echo "✗ kto_task_close: a summary is required" >&2; return 1; }
325
+
326
+ issue_json=$(gh issue view "$num" --repo "$repo" --json state,projectItems) || return 1
327
+ state=$(printf '%s' "$issue_json" | jq -r .state)
328
+
329
+ if kto_board_enabled && type load_project_ids >/dev/null 2>&1 && load_project_ids 2>/dev/null; then
330
+ item=$(project_find_item_by_issue "$num" 2>/dev/null || true)
331
+ [[ -n "$item" && -n "${STATUS_OPT_DONE:-}" ]] && \
332
+ project_set_single_select "$item" "$STATUS_FIELD_ID" "$STATUS_OPT_DONE" 2>/dev/null && \
333
+ echo "✓ Board Status → Done for #$num" || true
334
+ fi
335
+
336
+ if [[ "$state" == "OPEN" ]]; then
337
+ gh issue close "$num" --repo "$repo" --comment "$summary" && echo "✓ Closed issue #$num"
338
+ else
339
+ echo " issue #$num already $state — board status updated only"
340
+ fi
341
+ }
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env bash
2
+ # pr-evidence.sh — attach evidence (build/typecheck logs, screenshots) to a PR as a comment.
3
+ #
4
+ # A PR should carry the proof that its gates passed (the log, the rendered screen), not just a
5
+ # prose claim — the "evidence" half of a no-mistakes agentic workflow. This is the sourceable
6
+ # helper the kit-effort-pr / kit-task-pr flow calls after the PR is open.
7
+ #
8
+ # Source it: source scripts/lib/pr-evidence.sh
9
+ #
10
+ # Functions:
11
+ # pr_attach_evidence <pr-number> <evidence-file> [caption] post the evidence into the PR
12
+ # pr_evidence_usage print how kit-effort-pr should call it
13
+ #
14
+ # Best-effort by contract: a missing gh, missing args, or missing file warns + returns 0 so it
15
+ # NEVER hard-fails the caller. Callers should still append `|| true` for set -e safety.
16
+ #
17
+ # Requires: gh, jq not needed; uses git/coreutils. bash 3.2 compatible.
18
+ #
19
+ # Env:
20
+ # PR_EVIDENCE_REPO target repo in OWNER/REPO form; empty (default) = let gh resolve it
21
+ # from the current repo (omits --repo)
22
+ # KIT_EVIDENCE_URL_BASE host base for images — set it and an image embeds as ![caption](URL)
23
+ # KIT_EVIDENCE_MAX_BYTES inline truncation cap for text/log files (default 60000)
24
+
25
+ PR_EVIDENCE_REPO="${PR_EVIDENCE_REPO:-}"
26
+
27
+ # True when the file extension is a raster/vector image GitHub would render if uploaded.
28
+ _pr_evidence_is_image() {
29
+ case "$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" in
30
+ *.png|*.jpg|*.jpeg|*.gif|*.webp|*.svg|*.bmp|*.tif|*.tiff) return 0 ;;
31
+ *) return 1 ;;
32
+ esac
33
+ }
34
+
35
+ # Map a file extension to a fenced-code language hint.
36
+ _pr_evidence_lang() {
37
+ case "$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" in
38
+ *.json) echo json ;;
39
+ *.diff|*.patch) echo diff ;;
40
+ *.md|*.markdown) echo "" ;;
41
+ *.sh|*.bash) echo bash ;;
42
+ *.ts|*.tsx) echo ts ;;
43
+ *.js|*.jsx) echo js ;;
44
+ *) echo text ;;
45
+ esac
46
+ }
47
+
48
+ # Echo a backtick fence (>= 3) longer than any backtick run in the file, so log content
49
+ # that itself contains ``` cannot break out of the code block.
50
+ _pr_evidence_fence() {
51
+ local file="$1" longest n
52
+ longest=$(grep -oE '`+' "$file" 2>/dev/null | awk '{ if (length($0) > m) m = length($0) } END { print m+0 }')
53
+ n=3
54
+ [[ "${longest:-0}" -ge 3 ]] && n=$((longest + 1))
55
+ printf '%*s' "$n" '' | tr ' ' '`'
56
+ }
57
+
58
+ # Build the comment body for a text/log evidence file (inlined, fenced, truncation-aware).
59
+ _pr_evidence_text_body() {
60
+ local caption="$1" file="$2"
61
+ local max="${KIT_EVIDENCE_MAX_BYTES:-60000}"
62
+ local lang fence size
63
+ lang="$(_pr_evidence_lang "$file")"
64
+ fence="$(_pr_evidence_fence "$file")"
65
+ size=$(wc -c <"$file" 2>/dev/null | tr -d '[:space:]'); size="${size:-0}"
66
+
67
+ printf '## Evidence — %s\n\n' "$caption"
68
+ printf '_Source: `%s`_\n\n' "$(basename "$file")"
69
+ printf '%s%s\n' "$fence" "$lang"
70
+ if [[ "$size" -gt "$max" ]]; then
71
+ head -c "$max" "$file"
72
+ printf '\n%s\n\n' "$fence"
73
+ printf '_…truncated — showing the first %s of %s bytes. Full file: `%s`._\n' "$max" "$size" "$file"
74
+ else
75
+ cat "$file"
76
+ printf '\n%s\n' "$fence"
77
+ fi
78
+ }
79
+
80
+ # Build the comment body for an image evidence file.
81
+ # HONEST about the limitation: GitHub has no clean REST/CLI endpoint to upload a binary to a
82
+ # comment (uploads go through an undocumented, web-only multipart endpoint that gh does not
83
+ # expose). So an image is REFERENCED by local path + instructions; only when KIT_EVIDENCE_URL_BASE
84
+ # points at a host that already serves the file do we embed it with ![caption](URL).
85
+ _pr_evidence_image_body() {
86
+ local caption="$1" abs="$2" base="$3"
87
+ printf '## Evidence — %s\n\n' "$caption"
88
+ if [[ -n "${KIT_EVIDENCE_URL_BASE:-}" ]]; then
89
+ printf '![%s](%s/%s)\n\n' "$caption" "${KIT_EVIDENCE_URL_BASE%/}" "$base"
90
+ printf '_Embedded via `KIT_EVIDENCE_URL_BASE`. A broken image means the file is not hosted there yet._\n\n'
91
+ fi
92
+ printf '> Image evidence is **referenced, not uploaded**: GitHub exposes no clean REST/CLI\n'
93
+ printf '> endpoint to attach a binary to a comment (the upload path is an undocumented,\n'
94
+ printf '> web-only multipart endpoint that `gh` does not surface). To inline this image,\n'
95
+ printf '> drag-and-drop the file into the PR comment box in the browser, or host it and set\n'
96
+ printf '> `KIT_EVIDENCE_URL_BASE` so this helper embeds it.\n\n'
97
+ printf 'Local file: `%s`\n' "$abs"
98
+ }
99
+
100
+ # pr_attach_evidence <pr-number> <evidence-file> [caption]
101
+ # Post the evidence into the PR as a comment. Text/log files inline in a fenced block; images are
102
+ # referenced (see _pr_evidence_image_body). Caption defaults to the file basename.
103
+ pr_attach_evidence() {
104
+ local pr="${1:-}" file="${2:-}" caption="${3:-}"
105
+
106
+ if [[ -z "$pr" || -z "$file" ]]; then
107
+ echo "pr_attach_evidence: need <pr-number> <evidence-file> [caption] — skipping" >&2
108
+ pr_evidence_usage >&2
109
+ return 0
110
+ fi
111
+ if ! command -v gh >/dev/null 2>&1; then
112
+ echo "pr_attach_evidence: gh CLI not found — cannot post evidence to PR #$pr (skipping)" >&2
113
+ return 0
114
+ fi
115
+ if [[ ! -f "$file" ]]; then
116
+ echo "pr_attach_evidence: evidence file not found: $file (skipping)" >&2
117
+ return 0
118
+ fi
119
+ [[ -n "$caption" ]] || caption="$(basename "$file")"
120
+
121
+ local repo abs tmp
122
+ repo="${PR_EVIDENCE_REPO:-}"
123
+ abs="$(cd "$(dirname "$file")" 2>/dev/null && printf '%s/%s' "$(pwd)" "$(basename "$file")")"
124
+ [[ -n "$abs" ]] || abs="$file"
125
+
126
+ tmp="$(mktemp 2>/dev/null)" || { echo "pr_attach_evidence: mktemp failed (skipping)" >&2; return 0; }
127
+
128
+ if _pr_evidence_is_image "$file"; then
129
+ _pr_evidence_image_body "$caption" "$abs" "$(basename "$file")" >"$tmp"
130
+ else
131
+ _pr_evidence_text_body "$caption" "$file" >"$tmp"
132
+ fi
133
+
134
+ # Omit --repo when PR_EVIDENCE_REPO is empty so gh resolves the repo from the current directory.
135
+ local posted=1
136
+ if [[ -n "$repo" ]]; then
137
+ gh pr comment "$pr" --repo "$repo" --body-file "$tmp" >/dev/null 2>&1 && posted=0
138
+ else
139
+ gh pr comment "$pr" --body-file "$tmp" >/dev/null 2>&1 && posted=0
140
+ fi
141
+ if [[ "$posted" -eq 0 ]]; then
142
+ echo "✓ evidence attached to PR #$pr ($caption)" >&2
143
+ rm -f "$tmp"
144
+ return 0
145
+ fi
146
+ echo "✗ failed to post evidence comment to PR #$pr — body kept at $tmp" >&2
147
+ return 1
148
+ }
149
+
150
+ # pr_evidence_usage — how kit-effort-pr / kit-task-pr should call this helper.
151
+ pr_evidence_usage() {
152
+ cat <<'USAGE'
153
+ pr-evidence.sh — attach PR evidence (build/typecheck logs, screenshots) as a PR comment.
154
+
155
+ source scripts/lib/pr-evidence.sh
156
+ pr_attach_evidence <pr-number> <evidence-file> [caption]
157
+
158
+ kit-effort-pr should call it AFTER the PR is opened, once per gate artifact:
159
+
160
+ pr_num=$(gh pr list --head "$branch" --json number --jq '.[0].number')
161
+ pr_attach_evidence "$pr_num" build.log "build" || true
162
+ pr_attach_evidence "$pr_num" typecheck.log "typecheck" || true
163
+ pr_attach_evidence "$pr_num" screen.png "rendered — success state" || true
164
+
165
+ Text/log files are inlined in an auto-sized fenced block (truncated past
166
+ KIT_EVIDENCE_MAX_BYTES, default 60000). Images CANNOT be uploaded via gh/REST, so they
167
+ are referenced by local path + instructions; set KIT_EVIDENCE_URL_BASE to a host base
168
+ and the helper embeds ![caption](URL) instead.
169
+
170
+ Env: PR_EVIDENCE_REPO (OWNER/REPO; empty = gh resolves it from the current repo) ·
171
+ KIT_EVIDENCE_URL_BASE · KIT_EVIDENCE_MAX_BYTES (default 60000).
172
+ USAGE
173
+ }
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env bash
2
+ # project-scan.sh — agnostic project detection. Reports what cckit is pointed at, from the
3
+ # filesystem only (no baked-in project knowledge). Usage: source it && project_scan [dir]
4
+ project_scan() {
5
+ local dir="${1:-$PWD}" root stack=() kit="none"
6
+ root="$(git -C "$dir" rev-parse --show-toplevel 2>/dev/null || printf '%s' "$dir")"
7
+ [ -f "$root/package.json" ] && stack+=("node")
8
+ [ -f "$root/pyproject.toml" ] && stack+=("python")
9
+ [ -f "$root/go.mod" ] && stack+=("go")
10
+ [ -f "$root/Cargo.toml" ] && stack+=("rust")
11
+ if [ -f "$root/cckit.config.json" ]; then kit="configured"
12
+ elif [ -d "$root/.cckit" ]; then kit="partial"
13
+ elif [ -d "$root/.claude" ]; then kit="claude-only"; fi
14
+ printf '{"root":"%s","stack":[%s],"kit":"%s"}\n' \
15
+ "$root" "$(printf '"%s",' "${stack[@]:-}" | sed 's/,$//;s/""//')" "$kit"
16
+ }
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env bash
2
+ # claude-kit — React + framework detection for /kit-annotate.
3
+ # Source it, then call `react_detect [target_dir]`; it sets/exports REACT_* vars.
4
+ # Specificity-ordered: Next.js → React Router v7 (framework mode) → Vite → generic React.
5
+ # (A flat "is vite present?" check is unreliable — Next/RR apps often carry vite for tests.)
6
+ # Requires: jq. Safe to source (no `set -e`); never aborts the caller.
7
+
8
+ react_detect() {
9
+ local dir="${1:-$PWD}" pkg deps
10
+ # defaults
11
+ REACT_DETECTED="false"; REACT_FRAMEWORK=""; REACT_VERSION=""
12
+ REACT_ENTRY_FILE=""; REACT_PKG_MANAGER="npm"; REACT_ROUTER_LIB="false"
13
+ export REACT_DETECTED REACT_FRAMEWORK REACT_VERSION REACT_ENTRY_FILE REACT_PKG_MANAGER REACT_ROUTER_LIB
14
+
15
+ pkg="$dir/package.json"
16
+ [[ -f "$pkg" ]] || return 0
17
+ command -v jq >/dev/null 2>&1 || return 0
18
+
19
+ # merged dependency name list (deps + devDeps); tolerate malformed JSON
20
+ deps="$(jq -r '((.dependencies // {}) + (.devDependencies // {})) | keys[]?' "$pkg" 2>/dev/null)" || return 0
21
+ _has() { printf '%s\n' "$deps" | grep -qx "$1"; }
22
+
23
+ _has react || return 0
24
+ REACT_DETECTED="true"
25
+ REACT_VERSION="$(jq -r '(.dependencies.react // .devDependencies.react // "")' "$pkg" 2>/dev/null | tr -d '^~ ')"
26
+
27
+ # package manager from lockfile
28
+ if [[ -f "$dir/pnpm-lock.yaml" ]]; then REACT_PKG_MANAGER="pnpm"
29
+ elif [[ -f "$dir/yarn.lock" ]]; then REACT_PKG_MANAGER="yarn"
30
+ elif [[ -f "$dir/bun.lockb" ]]; then REACT_PKG_MANAGER="bun"
31
+ else REACT_PKG_MANAGER="npm"; fi
32
+
33
+ local has_next_cfg=false has_vite_cfg=false has_rr_cfg=false
34
+ ls "$dir"/next.config.* >/dev/null 2>&1 && has_next_cfg=true
35
+ ls "$dir"/vite.config.* >/dev/null 2>&1 && has_vite_cfg=true
36
+ ls "$dir"/react-router.config.* >/dev/null 2>&1 && has_rr_cfg=true
37
+
38
+ local f
39
+ if _has next || $has_next_cfg; then
40
+ # 1. Next.js (match first — Next apps frequently also list vite/vitest)
41
+ REACT_FRAMEWORK="next"
42
+ for f in app/layout.tsx app/layout.jsx app/layout.js src/app/layout.tsx \
43
+ pages/_app.tsx pages/_app.jsx pages/_app.js src/pages/_app.tsx; do
44
+ [[ -f "$dir/$f" ]] && { REACT_ENTRY_FILE="$f"; break; }
45
+ done
46
+ elif _has @react-router/dev || $has_rr_cfg; then
47
+ # 2. React Router v7 framework mode (Vite-based, but NOT a plain Vite SPA)
48
+ REACT_FRAMEWORK="react-router"
49
+ for f in app/root.tsx app/root.jsx app/root.js src/root.tsx; do
50
+ [[ -f "$dir/$f" ]] && { REACT_ENTRY_FILE="$f"; break; }
51
+ done
52
+ elif _has @vitejs/plugin-react || _has @vitejs/plugin-react-swc || { _has vite && $has_vite_cfg; }; then
53
+ # 3. Vite (plain React SPA)
54
+ REACT_FRAMEWORK="vite"
55
+ for f in src/main.tsx src/main.jsx src/index.tsx src/index.jsx src/main.ts; do
56
+ [[ -f "$dir/$f" ]] && { REACT_ENTRY_FILE="$f"; break; }
57
+ done
58
+ else
59
+ # 4. React present but framework unrecognized (CRA / Gatsby / Preact-compat / custom)
60
+ REACT_FRAMEWORK="react"
61
+ fi
62
+
63
+ # react-router(-dom) WITHOUT @react-router/dev = a routing library, not a framework target
64
+ if { _has react-router || _has react-router-dom; } && ! _has @react-router/dev; then
65
+ REACT_ROUTER_LIB="true"
66
+ fi
67
+
68
+ unset -f _has
69
+ return 0
70
+ }
71
+
72
+ # Run standalone → print a one-line summary (handy for --dry-run / debugging).
73
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
74
+ react_detect "${1:-$PWD}"
75
+ printf 'detected=%s framework=%s version=%s entry=%s pm=%s router_lib=%s\n' \
76
+ "$REACT_DETECTED" "$REACT_FRAMEWORK" "${REACT_VERSION:-?}" \
77
+ "${REACT_ENTRY_FILE:-?}" "$REACT_PKG_MANAGER" "$REACT_ROUTER_LIB"
78
+ fi
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env bash
2
+ # role-identity.sh — cheap per-role identity (NO separate GitHub accounts).
3
+ #
4
+ # Maps a role slug (designer, backend, tech-lead, …) to a git commit AUTHOR and a
5
+ # one-line signature. The GitHub actor stays the maintainer's token — this only sets
6
+ # the commit *author* and a signature line so a role's work READS as that role.
7
+ # Real distinct handles/avatars are a separate, heavier effort (bot accounts / GitHub App).
8
+ #
9
+ # Source it: source scripts/lib/role-identity.sh
10
+ #
11
+ # Tunables (env):
12
+ # KIT_ROLE_EMAIL_DOMAIN synthetic author-email domain (default: agents.local — not a real inbox)
13
+ # KIT_AGENT_LABEL suffix in the author/sig label (default: agent)
14
+ KIT_ROLE_EMAIL_DOMAIN="${KIT_ROLE_EMAIL_DOMAIN:-agents.local}"
15
+ KIT_AGENT_LABEL="${KIT_AGENT_LABEL:-agent}"
16
+
17
+ # role_display <slug> -> human label (empty for unknown/blank)
18
+ role_display() {
19
+ case "$1" in
20
+ designer) echo "Designer" ;;
21
+ devops) echo "DevOps" ;;
22
+ pm) echo "PM" ;;
23
+ frontend) echo "Frontend" ;;
24
+ tauri) echo "Tauri" ;;
25
+ ai-eng) echo "AI Eng" ;;
26
+ security) echo "Security" ;;
27
+ qa) echo "QA" ;;
28
+ tech-lead) echo "Tech Lead" ;;
29
+ research) echo "Research" ;;
30
+ backend) echo "Backend" ;;
31
+ *) echo "" ;;
32
+ esac
33
+ }
34
+
35
+ # role_git_author <slug> -> "Name|email" (empty when no/unknown role: caller keeps the default identity)
36
+ role_git_author() {
37
+ local d; d="$(role_display "$1")"
38
+ [[ -z "$d" ]] && return 0
39
+ printf '%s (%s)|%s@%s' "$d" "$KIT_AGENT_LABEL" "$1" "$KIT_ROLE_EMAIL_DOMAIN"
40
+ }
41
+
42
+ # role_signature <slug> -> one markdown line for issue/PR bodies (empty when no role)
43
+ role_signature() {
44
+ local d; d="$(role_display "$1")"
45
+ [[ -z "$d" ]] && return 0
46
+ printf -- '— %s · %s (acting via the maintainer account)' "$d" "$KIT_AGENT_LABEL"
47
+ }