@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,96 @@
1
+ #!/usr/bin/env bash
2
+ # secret-guard.sh — agnostic secret + privacy guard. Ensures cckit never publishes secrets,
3
+ # key material, env files, or YOUR private project data. NOTHING project-specific is hardcoded:
4
+ # the secret/key patterns are universal, and "what is private to me" is supplied by the user via
5
+ # an optional, gitignored denylist (.cckit/privacy-denylist) — cckit ships only an .example.
6
+ #
7
+ # Applies to EVERYTHING publishable — code, docs, cookbook, examples, templates.
8
+ # Usage: source secret-guard.sh && secret_guard_scan [file...] (default: git-tracked files)
9
+ # exit 0 = clean, 1 = a finding (with a report on stderr).
10
+
11
+ # Files that must never be committed (by basename). Env files include .env.example/.sample —
12
+ # even an example leaks your variable *names* and structure, so it stays local.
13
+ _sg_forbidden_name() {
14
+ case "$1" in
15
+ .env|.env.*|*.env) return 0 ;;
16
+ *.pem|*.key|*.p12|*.pfx|*.keystore|*.jks|id_rsa|id_rsa.*|id_ed25519|id_ed25519.*) return 0 ;;
17
+ .netrc|.pgpass|.npmrc|credentials|credentials.*|secrets.json|secrets.yaml|secrets.yml|*.tfvars) return 0 ;;
18
+ *.project-ids*|.project-ids.env) return 0 ;;
19
+ *) return 1 ;;
20
+ esac
21
+ }
22
+
23
+ # Universal high-signal secret content patterns (provider key prefixes, private-key blocks).
24
+ # Written so the patterns do not match their own literal text.
25
+ _sg_secret_patterns() {
26
+ cat <<'PAT'
27
+ -----BEGIN [A-Z0-9 ]*PRIVATE KEY-----
28
+ AKIA[0-9A-Z]{16}
29
+ ASIA[0-9A-Z]{16}
30
+ sk-[A-Za-z0-9]{20,}
31
+ sk-proj-[A-Za-z0-9_-]{20,}
32
+ gh[pousr]_[A-Za-z0-9]{36,}
33
+ github_pat_[A-Za-z0-9_]{50,}
34
+ AIza[0-9A-Za-z_-]{35}
35
+ xox[baprs]-[0-9A-Za-z-]{10,}
36
+ sk_live_[0-9A-Za-z]{24,}
37
+ rk_live_[0-9A-Za-z]{24,}
38
+ glpat-[0-9A-Za-z_-]{20,}
39
+ eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}
40
+ PAT
41
+ }
42
+
43
+ # Generic "assignment of a real-looking secret value" — allows placeholders (<...>, ${...},
44
+ # YOUR_, example, changeme, xxxx, redacted, placeholder).
45
+ _sg_assign_pattern='(api[_-]?key|secret|password|passwd|access[_-]?token|auth[_-]?token|client[_-]?secret|private[_-]?key)["'"'"' ]*[:=]+[ ]*["'"'"'][^"'"'"'$<{ ]{8,}'
46
+
47
+ secret_guard_scan() {
48
+ local files findings=0 f line denylist
49
+ if [ "$#" -gt 0 ]; then files=("$@"); else
50
+ # default: git-tracked + staged, minus this guard + the example denylist (avoid self-match)
51
+ mapfile -t files < <(git ls-files 2>/dev/null | grep -vE 'scripts/lib/secret-guard\.sh|privacy-denylist\.example' || true)
52
+ fi
53
+
54
+ # (a) forbidden filenames
55
+ for f in "${files[@]:-}"; do
56
+ [ -z "$f" ] && continue
57
+ if _sg_forbidden_name "$(basename "$f")"; then
58
+ echo "x secret-guard: forbidden file must not be committed: $f" >&2; findings=$((findings+1))
59
+ fi
60
+ done
61
+
62
+ # (b) universal secret content patterns
63
+ local pat
64
+ while IFS= read -r pat; do
65
+ [ -z "$pat" ] && continue
66
+ while IFS= read -r line; do
67
+ [ -z "$line" ] && continue
68
+ echo "x secret-guard: secret-like content: $line" >&2; findings=$((findings+1))
69
+ done < <(printf '%s\n' "${files[@]:-}" | tr '\n' '\0' | xargs -0 grep -nIE "$pat" 2>/dev/null | head -20)
70
+ done < <(_sg_secret_patterns)
71
+
72
+ # (c) generic secret assignment (placeholders allowed)
73
+ while IFS= read -r line; do
74
+ [ -z "$line" ] && continue
75
+ case "$line" in *YOUR_*|*example*|*EXAMPLE*|*placeholder*|*changeme*|*xxxx*|*redacted*|*'<'*|*'${'*) continue ;; esac
76
+ echo "x secret-guard: secret-like assignment: $line" >&2; findings=$((findings+1))
77
+ done < <(printf '%s\n' "${files[@]:-}" | tr '\n' '\0' | xargs -0 grep -niIE "$_sg_assign_pattern" 2>/dev/null | head -20)
78
+
79
+ # (d) user-supplied privacy denylist (agnostic: YOU declare what is yours; file stays local)
80
+ denylist=".cckit/privacy-denylist"
81
+ if [ -f "$denylist" ]; then
82
+ while IFS= read -r term; do
83
+ term="$(printf '%s' "$term" | sed 's/#.*//;s/^[[:space:]]*//;s/[[:space:]]*$//')"
84
+ [ -z "$term" ] && continue
85
+ while IFS= read -r line; do
86
+ [ -z "$line" ] && continue
87
+ echo "x secret-guard: private term '$term' present: $line" >&2; findings=$((findings+1))
88
+ done < <(printf '%s\n' "${files[@]:-}" | tr '\n' '\0' | xargs -0 grep -nIF "$term" 2>/dev/null | head -10)
89
+ done < "$denylist"
90
+ fi
91
+
92
+ if [ "$findings" -gt 0 ]; then
93
+ echo "✗ secret-guard: $findings finding(s) — refusing to proceed" >&2; return 1
94
+ fi
95
+ return 0
96
+ }
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+ # toon.sh - TOON (Token-Oriented Object Notation) encoding for context payloads. A uniform array of
3
+ # flat objects collapses to a compact tabular form - one keys header, then one row per element -
4
+ # which costs far fewer tokens than repeating every key in JSON. Anything that is not a uniform,
5
+ # scalar-only array of objects (or is below the size gate) falls back to compact JSON unchanged.
6
+ #
7
+ # toon_encode read JSON on stdin, write TOON (or JSON fallback) on stdout
8
+ # TOON_MIN_ROWS=N size gate: arrays shorter than N stay JSON (default 2)
9
+
10
+ # toon_encode - stdin JSON -> stdout TOON-or-JSON.
11
+ toon_encode() {
12
+ command -v jq >/dev/null 2>&1 || { cat; return 0; }
13
+ local json n keys uniform min="${TOON_MIN_ROWS:-2}"
14
+ json="$(cat)"
15
+
16
+ n="$(printf '%s' "$json" | jq 'if type=="array" then length else -1 end' 2>/dev/null || echo -1)"
17
+ # not an array, or smaller than the gate -> compact JSON (TOON overhead not worth it).
18
+ if [ "$n" -lt "$min" ]; then printf '%s' "$json" | jq -c . 2>/dev/null || printf '%s\n' "$json"; return 0; fi
19
+
20
+ keys="$(printf '%s' "$json" | jq -r '.[0] | (keys_unsorted | join(","))' 2>/dev/null || true)"
21
+ # uniform = every element has the same key order AND only scalar values (tabular-encodable).
22
+ uniform="$(printf '%s' "$json" | jq -r --arg k "$keys" '
23
+ all(.[];
24
+ (keys_unsorted | join(",")) == $k
25
+ and ([.[]] | all(type | . == "string" or . == "number" or . == "boolean" or . == "null")))
26
+ ' 2>/dev/null || echo false)"
27
+
28
+ if [ "$uniform" != "true" ] || [ -z "$keys" ]; then
29
+ printf '%s' "$json" | jq -c . # non-uniform / nested -> JSON fallback
30
+ return 0
31
+ fi
32
+
33
+ printf '[%s]{%s}:\n' "$n" "$keys"
34
+ printf '%s' "$json" | jq -r '.[] | [.[]] | @csv'
35
+ }
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env bash
2
+ # ui.sh - terminal ergonomics, all detect-or-fallback (never a hard dependency, never required).
3
+ # Color is gated on a real TTY + NO_COLOR; optional tools (glow, fzf, gum) enhance output when
4
+ # present and degrade silently when not. Keeping this in one place gives every verb consistent,
5
+ # pipe-safe behavior: piped or non-tty output is always plain.
6
+
7
+ # ui_tty - true only when stdout is a real terminal (so pipes/redirects stay plain).
8
+ ui_tty() { [ -t 1 ]; }
9
+
10
+ # ui_color - true when we should emit ANSI color. NO_COLOR always wins (disables). Otherwise
11
+ # CCKIT_FORCE_COLOR forces color even without a TTY (screenshots, CI, demos); failing that, color
12
+ # needs a real TTY with a non-"dumb" TERM.
13
+ #
14
+ # We deliberately use a cckit-PRIVATE variable rather than the conventional CLICOLOR_FORCE/
15
+ # FORCE_COLOR: those leak into the subprocesses cckit shells out to, and `gh` (mis)honors
16
+ # CLICOLOR_FORCE by coloring even its `--json` output, which corrupts the JSON cckit then pipes to
17
+ # jq. A private flag colors cckit's own output without poisoning child-process machine output.
18
+ ui_color() {
19
+ [ -z "${NO_COLOR:-}" ] || return 1
20
+ [ -n "${CCKIT_FORCE_COLOR:-}" ] && return 0
21
+ ui_tty && [ "${TERM:-}" != "dumb" ]
22
+ }
23
+
24
+ # ui_paint <ansi-code> <text> - color text when ui_color, else plain.
25
+ ui_paint() {
26
+ if ui_color; then printf '\033[%sm%s\033[0m' "$1" "$2"; else printf '%s' "$2"; fi
27
+ }
28
+
29
+ # ui_page - pretty-render markdown on stdin with glow when present + interactive; else cat.
30
+ ui_page() {
31
+ if ui_tty && command -v glow >/dev/null 2>&1; then glow -; else cat; fi
32
+ }
33
+
34
+ # ui_pick <prompt> - choose one line from stdin: fzf if present + interactive, else first line.
35
+ # Echoes the chosen line on stdout.
36
+ ui_pick() {
37
+ if ui_tty && command -v fzf >/dev/null 2>&1; then
38
+ fzf --prompt="${1:-pick> }" --height=40% --reverse
39
+ else
40
+ head -n1
41
+ fi
42
+ }
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env bash
2
+ # version-bump.sh - SemVer + Conventional Commits. ONE implementation, shared by `cckit release`
3
+ # and the release workflow: compute the next version from the commits since the last tag, or write
4
+ # a version into the project's manifests.
5
+ #
6
+ # version-bump.sh --next echo the next version (no side effects)
7
+ # version-bump.sh --emit echo `bump=<level>` + `next=<version>` (for $GITHUB_OUTPUT)
8
+ # version-bump.sh --write <version> set the version in cckit.config.json + plugin.json + package.json
9
+ #
10
+ # Bump rules (Conventional Commits): a `feat!:`/`type!:` or a `BREAKING CHANGE` footer -> major;
11
+ # `feat:` -> minor; `fix|perf|refactor|revert:` -> patch; non-functional types
12
+ # (`docs|chore|style|test|ci|build`) and anything else -> none (no release).
13
+ # no commits -> none (no release).
14
+ set -euo pipefail
15
+ ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
16
+
17
+ vb_current() { git -C "$ROOT" describe --tags --abbrev=0 2>/dev/null | sed 's/^v//'; }
18
+
19
+ vb_level() {
20
+ local range="$1" log
21
+ log="$(git -C "$ROOT" log --format='%s%n%b' $range 2>/dev/null || true)"
22
+ [ -n "$log" ] || { echo none; return; }
23
+ if printf '%s\n' "$log" | grep -qE '(^|[[:space:]])BREAKING CHANGE|^[a-z]+(\([^)]*\))?!:'; then echo major; return; fi
24
+ if printf '%s\n' "$log" | grep -qE '^feat(\([^)]*\))?:'; then echo minor; return; fi
25
+ if printf '%s\n' "$log" | grep -qE '^(fix|perf|refactor|revert)(\([^)]*\))?:'; then echo patch; return; fi
26
+ # Only non-functional commits (docs/chore/style/test/ci/build, or untyped) -> no release.
27
+ echo none
28
+ }
29
+
30
+ vb_next() {
31
+ local cur="${1:-0.0.0}" level="$2" M m p rest
32
+ M="${cur%%.*}"; rest="${cur#*.}"; m="${rest%%.*}"; p="${rest#*.}"
33
+ case "$level" in
34
+ major) echo "$((M+1)).0.0" ;;
35
+ minor) echo "$M.$((m+1)).0" ;;
36
+ patch) echo "$M.$m.$((p+1))" ;;
37
+ *) echo "$cur" ;;
38
+ esac
39
+ }
40
+
41
+ vb_write() {
42
+ local v="$1" f
43
+ command -v jq >/dev/null || { echo "version-bump: jq required to --write" >&2; return 1; }
44
+ for f in cckit.config.json .claude-plugin/plugin.json package.json docs-site/package.json; do
45
+ [ -f "$ROOT/$f" ] || continue
46
+ if [ "$f" = "cckit.config.json" ]; then jq --arg v "$v" '.kitVersion=$v' "$ROOT/$f" > "$ROOT/$f.tmp"
47
+ else jq --arg v "$v" '.version=$v' "$ROOT/$f" > "$ROOT/$f.tmp"; fi
48
+ mv "$ROOT/$f.tmp" "$ROOT/$f" && echo " set $f -> $v"
49
+ done
50
+ }
51
+
52
+ _range() { local cur; cur="$(vb_current)"; if [ -n "$cur" ]; then echo "v$cur..HEAD"; else echo "HEAD"; fi; }
53
+
54
+ case "${1:-}" in
55
+ --next) vb_next "$(vb_current)" "$(vb_level "$(_range)")" ;;
56
+ --emit) printf 'bump=%s\n' "$(vb_level "$(_range)")"; printf 'next=%s\n' "$(vb_next "$(vb_current)" "$(vb_level "$(_range)")")" ;;
57
+ --write) vb_write "${2:?usage: --write <version>}" ;;
58
+ *) echo "usage: version-bump.sh --next | --emit | --write <version>" >&2; exit 2 ;;
59
+ esac
@@ -0,0 +1,45 @@
1
+ #!/bin/sh
2
+ # worktree-issue-test.sh — self-test for worktree-issue.sh under bash AND zsh (#307).
3
+ # The lib is sourced into whatever shell the session runs (zsh on macOS), so the
4
+ # parsing must behave identically in both. Run: bash scripts/lib/worktree-issue-test.sh
5
+ #
6
+ # Without args: re-runs itself under every available shell. With WT_TEST_INNER set:
7
+ # runs the assertions in the current interpreter.
8
+
9
+ dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
10
+
11
+ if [ -n "${WT_TEST_INNER:-}" ]; then
12
+ . "$dir/worktree-issue.sh"
13
+ fail=0
14
+ check() {
15
+ got=$(wt_issue_number "$1")
16
+ if [ "$got" != "$2" ]; then
17
+ echo "FAIL($WT_TEST_INNER): wt_issue_number '$1' -> '[$got]', want '[$2]'"
18
+ fail=1
19
+ fi
20
+ }
21
+ check "task/293-mobbin" "293" # branch
22
+ check "feat/46-roadmap-view" "46" # branch
23
+ check "fix/305-session-registry-phantom" "305" # branch
24
+ check ".claude/worktrees/task+293-mobbin" "293" # worktree path
25
+ check "task+293-mobbin" "293" # worktree dirname
26
+ check "/abs/path/.claude/worktrees/plan+266-ui" "266" # absolute worktree path
27
+ check "agent/foo" "" # bot branch — no number
28
+ check "claude/issue-42" "" # bot branch — slug not kind/N-
29
+ check "develop" "" # base branch
30
+ check "main" "" # base branch
31
+ check "task/29a3-x" "" # malformed number
32
+ check "task/293mobbin" "" # missing dash
33
+ [ "$fail" -eq 0 ] && echo "OK($WT_TEST_INNER): 12 cases"
34
+ exit "$fail"
35
+ fi
36
+
37
+ rc=0
38
+ ran=0
39
+ for sh in bash zsh; do
40
+ command -v "$sh" >/dev/null 2>&1 || continue
41
+ ran=1
42
+ WT_TEST_INNER="$sh" "$sh" "$0" || rc=1
43
+ done
44
+ [ "$ran" -eq 1 ] || { echo "no bash/zsh found"; exit 1; }
45
+ exit "$rc"
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env bash
2
+ # worktree-issue.sh — associate a branch / worktree with the GitHub issue it belongs to,
3
+ # and report whether that issue is still open. The association is DERIVED from the name —
4
+ # there is no hand-maintained registry.
5
+ #
6
+ # Conventions (see .claude/rules/branch-naming.md):
7
+ # branch <kind>/<N>-<slug> e.g. feat/173-admin-clerk-signin
8
+ # worktree dir <kind>+<N>-<slug> e.g. .claude/worktrees/feat+173-admin-clerk-signin
9
+ #
10
+ # Source it: source scripts/lib/worktree-issue.sh
11
+ # Requires: git; gh (for state lookups — degrades to "unknown" without it).
12
+ # Portable: POSIX parameter expansion only — sourceable from bash 3.2+ AND zsh (#307;
13
+ # BASH_REMATCH stays silently empty in zsh, which switched the gc protection off).
14
+ # Self-test: bash scripts/lib/worktree-issue-test.sh (runs the cases under bash + zsh)
15
+
16
+ # Echo the issue number a branch name / worktree path belongs to (empty if none, e.g. bot branches).
17
+ wt_issue_number() {
18
+ local ref="$1" base kind rest n
19
+ # branch form: kind/N-slug (kind = lowercase letters only, anchored at start)
20
+ kind="${ref%%/*}"; rest="${ref#*/}"
21
+ if [ "$kind" != "$ref" ]; then # ref contains "/"
22
+ case "$kind" in
23
+ ''|*[!a-z]*) : ;; # not a pure-lowercase kind
24
+ *)
25
+ n="${rest%%-*}"
26
+ if [ "$n" != "$rest" ]; then # a "-" follows the number
27
+ case "$n" in
28
+ ''|*[!0-9]*) : ;;
29
+ *) printf '%s' "$n"; return 0 ;;
30
+ esac
31
+ fi
32
+ ;;
33
+ esac
34
+ fi
35
+ # worktree-dir form: .../kind+N-slug
36
+ base="${ref##*/}"
37
+ kind="${base%%+*}"; rest="${base#*+}"
38
+ if [ "$kind" != "$base" ]; then # basename contains "+"
39
+ case "$kind" in
40
+ ''|*[!a-z]*) return 0 ;;
41
+ esac
42
+ n="${rest%%-*}"
43
+ [ "$n" = "$rest" ] && return 0 # no "-" after the number
44
+ case "$n" in
45
+ ''|*[!0-9]*) : ;;
46
+ *) printf '%s' "$n" ;;
47
+ esac
48
+ fi
49
+ return 0
50
+ }
51
+
52
+ # Echo the issue state ("open" / "closed") for an issue number, lowercased. Empty when unknown
53
+ # (no number, no gh, or lookup failed) — callers must treat empty as "don't assume safe to delete".
54
+ wt_issue_state() {
55
+ local n="$1" repo="${2:-}"
56
+ [[ -z "$n" ]] && return 0
57
+ command -v gh >/dev/null 2>&1 || return 0
58
+ local args=(issue view "$n" --json state --jq .state)
59
+ [[ -n "$repo" ]] && args+=(--repo "$repo")
60
+ gh "${args[@]}" 2>/dev/null | tr '[:upper:]' '[:lower:]'
61
+ }
62
+
63
+ # Echo a protection reason if a branch/ref must NOT be garbage-collected because its issue
64
+ # is still open; empty string means "no issue-based protection" (other gc rules still apply).
65
+ wt_protected_reason() {
66
+ local ref="$1" repo="${2:-}" n state
67
+ n="$(wt_issue_number "$ref")"
68
+ [[ -z "$n" ]] && return 0
69
+ state="$(wt_issue_state "$n" "$repo")"
70
+ if [[ "$state" == "open" ]]; then
71
+ printf 'issue #%s still OPEN' "$n"
72
+ fi
73
+ }
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env bash
2
+ # worktree-start.sh — the canonical "start a worktree for an issue" git-mechanic.
3
+ #
4
+ # Family 1 of kit-engine-boundary.md (rule #1/#2): one bash home for the op, consumed by the
5
+ # kit-task-start skill, scripts/orchestrate.sh, and `kit task start`. No second implementation.
6
+ #
7
+ # wt_start <issue-number> [slug-override]
8
+ # stdout: "<worktree-path>|<branch>|<issue-number>" (one line, machine-readable)
9
+ # stderr: human progress
10
+ # returns: 0 on success (created or reused), 1 on failure
11
+ #
12
+ # Requires: gh, jq, git, scripts/lib/gh-project.sh (board update). bash 3.2 compatible.
13
+
14
+ WT_START_REPO="${WT_START_REPO:-jeiemgi/cckit}"
15
+
16
+ # _wt_set_port <app-env-file> <port> <issue-num> — append a per-worktree dev PORT to an app's
17
+ # .env.local, but only where one exists (i.e. the app is locally runnable). Idempotent: an existing
18
+ # PORT= line wins. The app dev scripts read ${PORT:-300X} (sub B) so this assignment takes effect.
19
+ _wt_set_port() {
20
+ local file="$1" port="$2" num="$3"
21
+ [[ -f "$file" ]] || return 0
22
+ grep -q '^PORT=' "$file" 2>/dev/null && return 0
23
+ printf '\n# kit worktree #%s — per-worktree dev port (#773)\nPORT=%s\n' "$num" "$port" >> "$file"
24
+ echo "[#$num] PORT=$port -> $file" >&2
25
+ }
26
+
27
+ # wt_assign_ports <worktree> <issue-num> <root> — assign a per-worktree dev PORT to each app whose
28
+ # env file is listed in `.worktree.devPorts` of <root>/.claude/kit.config.json. Each entry is
29
+ # {path, base}; the port = base + (issue % 40) * <count> so lanes stay disjoint within and across
30
+ # worktrees. Config-driven (no hardcoded app paths) so the kit stays portable: a project with no
31
+ # `.worktree.devPorts` (or no kit.config.json) is a silent no-op. bash 3.2.
32
+ wt_assign_ports() {
33
+ local wt="$1" num="$2" root="$3" cfg ports n offset i path base
34
+ cfg="$root/.claude/kit.config.json"
35
+ # jq is a stated requirement of this file (see header) — don't pre-check `command -v jq` here: the
36
+ # `command -v … || return` idiom mis-fires under zsh, and the jq read below already no-ops on a
37
+ # missing config / missing jq. Guard only on the config file existing.
38
+ [[ -f "$cfg" ]] || return 0
39
+ ports="$(jq -c '.worktree.devPorts // []' "$cfg" 2>/dev/null)" || return 0
40
+ [[ -n "$ports" && "$ports" != "[]" ]] || return 0
41
+ n="$(jq 'length' <<<"$ports" 2>/dev/null)"; [[ "$n" =~ ^[0-9]+$ && "$n" -gt 0 ]] || return 0
42
+ offset=$(( num % 40 )); i=0
43
+ while [[ "$i" -lt "$n" ]]; do
44
+ path="$(jq -r ".[$i].path // empty" <<<"$ports")"
45
+ base="$(jq -r ".[$i].base // empty" <<<"$ports")"
46
+ [[ -n "$path" && "$base" =~ ^[0-9]+$ ]] && _wt_set_port "$wt/$path" $(( base + offset * n )) "$num"
47
+ i=$(( i + 1 ))
48
+ done
49
+ }
50
+
51
+ # wt_bootstrap <root> <worktree> <issue-num> — make a fresh worktree runnable for local dev.
52
+ # A new worktree inherits no .gitignored local config and no node_modules, and parallel worktrees
53
+ # collide on the hardcoded dev port. This copies the local env, installs deps, and assigns a
54
+ # per-worktree dev port. Every step is best-effort + idempotent — never fail the start (#773).
55
+ wt_bootstrap() {
56
+ local root="$1" wt="$2" num="$3" rel src dst offset
57
+ [[ -d "$wt" ]] || return 0
58
+
59
+ # 1. Copy gitignored local config the worktree can't inherit: every .env.local* + project ids.
60
+ while IFS= read -r rel; do
61
+ [[ -n "$rel" ]] || continue
62
+ src="$root/$rel"; dst="$wt/$rel"
63
+ [[ -f "$src" ]] || continue
64
+ [[ -f "$dst" ]] && continue # idempotent: never clobber edits already made in the worktree
65
+ mkdir -p "$(dirname "$dst")"
66
+ cp "$src" "$dst" && echo "[#$num] copied $rel" >&2
67
+ done < <(cd "$root" && {
68
+ find . -name '.env.local*' \
69
+ -not -path './node_modules/*' -not -path './.git/*' -not -path './.claude/worktrees/*' 2>/dev/null
70
+ [[ -f scripts/.project-ids.env ]] && echo './scripts/.project-ids.env'
71
+ } | sed 's|^\./||' | sort -u)
72
+
73
+ # 2. Assign a per-worktree dev PORT per app (base + offset*lanes from the issue number) so two
74
+ # worktrees never fight for the same port. The app→base map is config-driven
75
+ # (`.worktree.devPorts` in kit.config.json) so the kit carries no hardcoded app paths.
76
+ wt_assign_ports "$wt" "$num" "$root"
77
+
78
+ # 3. Install deps — node_modules is per-worktree, not shared. Opt out with KIT_WT_INSTALL=0.
79
+ if [[ "${KIT_WT_INSTALL:-1}" != "0" ]] && command -v pnpm >/dev/null 2>&1; then
80
+ echo "[#$num] pnpm install (set KIT_WT_INSTALL=0 to skip)..." >&2
81
+ ( cd "$wt" && pnpm install --prefer-offline >/dev/null 2>&1 ) \
82
+ && echo "[#$num] deps installed" >&2 \
83
+ || echo "[#$num] pnpm install failed — run 'pnpm install' in the worktree manually" >&2
84
+ fi
85
+ }
86
+
87
+ # ── Idle-worktree pool (OPT-IN, KIT_WT_POOL=1) ──────────────────────────────────────────────
88
+ # Treehouse-style reuse: instead of always creating a fresh worktree, recycle an IDLE one whose
89
+ # work already landed — saving the `git worktree add` + env copy + dependency install. OFF by
90
+ # default: with KIT_WT_POOL unset/0, wt_start takes the exact same create path as before and none
91
+ # of these helpers run. A worktree is REUSABLE only when ALL of these hold (conservative — if
92
+ # unsure, don't reuse; fall through to create):
93
+ # • it lives under .claude/worktrees/ (a pooled tree — never the main checkout or the target)
94
+ # • it is not locked
95
+ # • its branch is already merged into origin/${KIT_BASE_BRANCH:-main} (the committed work landed — recycling it
96
+ # destroys nothing; the old branch ref survives in the object store regardless)
97
+ # • its working tree is clean (no staged/unstaged/untracked changes — recover-before-prune)
98
+ # • no LIVE session owns it (kit-sessions registry: no live pid sitting in that dir)
99
+
100
+ # _wt_mtime <path> — file mtime as epoch seconds (BSD stat, then GNU stat). Picks the oldest tree.
101
+ _wt_mtime() { stat -f %m "$1" 2>/dev/null || stat -c %Y "$1" 2>/dev/null; }
102
+
103
+ # _wt_list <root> — emit one "<path>\t<branch-or-->\t<locked:0|1>" line per worktree (porcelain).
104
+ _wt_list() {
105
+ local root="$1" line wt_path="" branch="" locked="0"
106
+ while IFS= read -r line; do
107
+ case "$line" in
108
+ "worktree "*) wt_path="${line#worktree }" ;;
109
+ "branch refs/heads/"*) branch="${line#branch refs/heads/}" ;;
110
+ "detached") branch="-" ;;
111
+ "locked"*) locked="1" ;;
112
+ "") [[ -n "$wt_path" ]] && printf '%s\t%s\t%s\n' "$wt_path" "${branch:--}" "$locked"
113
+ wt_path=""; branch=""; locked="0" ;;
114
+ esac
115
+ done < <(git -C "$root" worktree list --porcelain 2>/dev/null)
116
+ [[ -n "$wt_path" ]] && printf '%s\t%s\t%s\n' "$wt_path" "${branch:--}" "$locked"
117
+ }
118
+
119
+ # _wt_branch_merged <root> <branch> — true when <branch> is an ancestor of origin/${KIT_BASE_BRANCH:-main} (landed).
120
+ _wt_branch_merged() {
121
+ local root="$1" branch="$2"
122
+ [[ -n "$branch" && "$branch" != "-" ]] || return 1
123
+ git -C "$root" merge-base --is-ancestor "refs/heads/$branch" origin/${KIT_BASE_BRANCH:-main} 2>/dev/null
124
+ }
125
+
126
+ # _wt_is_clean <path> — true only when the worktree exists AND has no staged/unstaged/untracked
127
+ # changes. A missing/zombie dir (git -C fails) is reported NOT clean, so it can never be recycled.
128
+ _wt_is_clean() {
129
+ local out
130
+ out="$(git -C "$1" status --porcelain 2>/dev/null)" || return 1
131
+ [[ -z "$out" ]]
132
+ }
133
+
134
+ # _wt_session_owns <root> <path> — true when a LIVE Claude session sits in <path> (or a subdir),
135
+ # per the kit-sessions registry (.git/kit-sessions/*.json, written by session-registry.sh). A dead
136
+ # pid is not an owner. No registry → no known live owner (returns false).
137
+ _wt_session_owns() {
138
+ local root="$1" wt_path="$2" common reg f opid ocwd
139
+ common="$(git -C "$root" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)" || return 1
140
+ reg="$common/kit-sessions"
141
+ [[ -d "$reg" ]] || return 1
142
+ for f in "$reg"/*.json; do
143
+ [[ -e "$f" ]] || continue
144
+ case "$f" in "$reg"/cache-*) continue ;; esac
145
+ opid="$(jq -r '.pid // 0' "$f" 2>/dev/null)"
146
+ [[ "$opid" =~ ^[1-9][0-9]*$ ]] || continue
147
+ kill -0 "$opid" 2>/dev/null || continue
148
+ ocwd="$(jq -r '.cwd // empty' "$f" 2>/dev/null)"
149
+ case "$ocwd" in "$wt_path"|"$wt_path"/*) return 0 ;; esac
150
+ done
151
+ return 1
152
+ }
153
+
154
+ # _wt_pool_find <root> <target> — path of the OLDEST reusable pooled worktree, or nothing. Applies
155
+ # the full eligibility gate above; <target> (the path wt_start is about to use) is always excluded.
156
+ _wt_pool_find() {
157
+ local root="$1" target="$2" wtdir best="" best_mt="" wt_path branch locked mt
158
+ wtdir="$root/.claude/worktrees/"
159
+ while IFS=$'\t' read -r wt_path branch locked; do
160
+ [[ -n "$wt_path" ]] || continue
161
+ case "$wt_path" in "$wtdir"*) : ;; *) continue ;; esac
162
+ [[ "$wt_path" == "$target" ]] && continue
163
+ [[ -d "$wt_path" ]] || continue
164
+ [[ "$locked" == "1" ]] && continue
165
+ _wt_branch_merged "$root" "$branch" || continue
166
+ _wt_is_clean "$wt_path" || continue
167
+ _wt_session_owns "$root" "$wt_path" && continue
168
+ mt="$(_wt_mtime "$wt_path")"; [[ "$mt" =~ ^[0-9]+$ ]] || mt=0
169
+ if [[ -z "$best" || "$mt" -lt "$best_mt" ]]; then best="$wt_path"; best_mt="$mt"; fi
170
+ done < <(_wt_list "$root")
171
+ [[ -n "$best" ]] && printf '%s\n' "$best"
172
+ }
173
+
174
+ # wt_pool_status — diagnostic listing of pooled worktrees + their reuse signals (read-only; never
175
+ # mutates). Shows branch / merged-into-develop? / clean? / live-session? / overall reusable?.
176
+ wt_pool_status() {
177
+ local root wtdir wt_path branch locked merged clean owned reusable any=0
178
+ root="$(git worktree list --porcelain | awk '/^worktree /{print $2; exit}')"
179
+ [[ -n "$root" ]] || { echo "wt_pool_status: not in a git repo" >&2; return 1; }
180
+ wtdir="$root/.claude/worktrees/"
181
+ git -C "$root" fetch origin "${KIT_BASE_BRANCH:-main}" --quiet 2>/dev/null || true
182
+ printf '%-44s %-26s %-7s %-6s %-8s %s\n' "WORKTREE" "BRANCH" "MERGED" "CLEAN" "SESSION" "REUSABLE"
183
+ while IFS=$'\t' read -r wt_path branch locked; do
184
+ [[ -n "$wt_path" ]] || continue
185
+ case "$wt_path" in "$wtdir"*) : ;; *) continue ;; esac
186
+ any=1
187
+ _wt_branch_merged "$root" "$branch" && merged="yes" || merged="no"
188
+ _wt_is_clean "$wt_path" && clean="yes" || clean="no"
189
+ _wt_session_owns "$root" "$wt_path" && owned="live" || owned="-"
190
+ if [[ "$locked" != "1" && "$merged" == "yes" && "$clean" == "yes" && "$owned" == "-" ]]; then
191
+ reusable="yes"
192
+ else
193
+ reusable="no"
194
+ fi
195
+ printf '%-44s %-26s %-7s %-6s %-8s %s\n' "${wt_path#"$wtdir"}" "$branch" "$merged" "$clean" "$owned" "$reusable"
196
+ done < <(_wt_list "$root")
197
+ [[ "$any" == "1" ]] || echo "(no pooled worktrees under $wtdir)"
198
+ }
199
+
200
+ wt_start() {
201
+ local num="${1:-}" slug_override="${2:-}" root meta title kind slug branch wt reused cand
202
+ [[ -n "$num" ]] || { echo "wt_start: issue number required" >&2; return 1; }
203
+
204
+ root="$(git worktree list --porcelain | awk '/^worktree /{print $2; exit}')"
205
+ [[ -n "$root" ]] || { echo "wt_start: not in a git repo" >&2; return 1; }
206
+
207
+ # The board's Status is set server-side by built-in automations (Item closed→Done, PR
208
+ # merged→Done) — wt_start no longer writes In Progress, so no board helpers are loaded here.
209
+
210
+ meta="$(gh issue view "$num" --repo "$WT_START_REPO" --json title,labels 2>/dev/null)" \
211
+ || { echo "[#$num] issue not found" >&2; return 1; }
212
+ title="$(echo "$meta" | jq -r '.title')"
213
+ kind="$(echo "$meta" | jq -r '([.labels[].name | select(startswith("kind:"))][0] // "kind:task") | sub("^kind:";"")')"
214
+ if [[ -n "$slug_override" ]]; then
215
+ slug="$slug_override"
216
+ else
217
+ slug="$(echo "$title" \
218
+ | sed -E 's/^\[[^]]+\][[:space:]]*//' \
219
+ | tr '[:upper:]' '[:lower:]' \
220
+ | sed -E 's/[^a-z0-9]+/-/g; s/^-+|-+$//g' \
221
+ | cut -c1-40)"
222
+ fi
223
+ branch="$kind/$num-$slug"
224
+ wt="$root/.claude/worktrees/${kind}+${num}-${slug}"
225
+
226
+ git -C "$root" fetch origin "${KIT_BASE_BRANCH:-main}" --quiet
227
+ if git -C "$root" worktree list --porcelain | grep -q "/${kind}+${num}-${slug}$"; then
228
+ echo "[#$num] reusing worktree $wt" >&2
229
+ else
230
+ reused=""
231
+ # OPT-IN (KIT_WT_POOL=1): recycle an idle, already-landed worktree instead of creating one.
232
+ # Safe by construction: _wt_pool_find only returns a clean, merged, session-free tree, and we
233
+ # only attempt it when the new branch does not yet exist — so re-pointing can't collide with a
234
+ # branch checked out elsewhere. Any hiccup leaves `reused` empty and falls through to the
235
+ # unchanged create path below; with the flag off this whole block is skipped.
236
+ if [[ "${KIT_WT_POOL:-0}" == "1" ]] && ! git -C "$root" show-ref --verify --quiet "refs/heads/$branch"; then
237
+ cand="$(_wt_pool_find "$root" "$wt")"
238
+ if [[ -n "$cand" ]] && git -C "$root" worktree move "$cand" "$wt" >/dev/null 2>&1; then
239
+ # The recycled tree is now at the conventional path with its env + dependencies intact
240
+ # (they moved with the dir — the pool's whole payoff). Re-point it to a fresh branch off
241
+ # the latest develop; the clean+merged+branch-absent preconditions make -B reliable.
242
+ git -C "$wt" checkout -B "$branch" origin/${KIT_BASE_BRANCH:-main} >/dev/null 2>&1 || true
243
+ reused="1"
244
+ echo "[#$num] reused idle worktree $wt" >&2
245
+ fi
246
+ fi
247
+ if [[ -z "$reused" ]]; then
248
+ git -C "$root" worktree add -B "$branch" "$wt" origin/${KIT_BASE_BRANCH:-main} >/dev/null 2>&1 \
249
+ || { echo "[#$num] worktree add failed (branch $branch may exist elsewhere)" >&2; return 1; }
250
+ echo "[#$num] created worktree $wt (branch $branch)" >&2
251
+ fi
252
+ fi
253
+
254
+ # Bootstrap the worktree for local dev: copy local env, install deps, assign a dev port (#773).
255
+ # Best-effort — a bootstrap hiccup never fails the start.
256
+ wt_bootstrap "$root" "$wt" "$num" || true
257
+
258
+ # Register with zoxide so `kit cd <issue|slug>` can jump here (no-op when zoxide is absent).
259
+ command -v zoxide >/dev/null && zoxide add "$wt" >/dev/null 2>&1 || true
260
+
261
+ # Mark the issue In Progress on the board. The board's built-in automations own the other
262
+ # transitions server-side (Item added→Todo, PR linked→In Review, closed/merged→Done) — but GitHub
263
+ # has no "branch started" trigger, so the kit owns In Progress. Cheap now: an O(1) issue.projectItems
264
+ # lookup on the org board, not a full-board scan. Best-effort — a board hiccup never fails the start.
265
+ if source "$root/scripts/lib/gh-project.sh" 2>/dev/null; then
266
+ [[ -n "${STATUS_FIELD_ID:-}" ]] || load_project_ids >/dev/null 2>&1 || true
267
+ item="$(project_find_item_by_issue "$num" 2>/dev/null)"
268
+ if [[ -z "$item" ]]; then
269
+ local content_id
270
+ content_id="$(gh api graphql -f query='query($o:String!,$r:String!,$n:Int!){repository(owner:$o,name:$r){issue(number:$n){id}}}' \
271
+ -F o="${WT_START_REPO%/*}" -F r="${WT_START_REPO#*/}" -F n="$num" --jq '.data.repository.issue.id' 2>/dev/null)"
272
+ [[ -n "$content_id" ]] && item="$(project_add_item "$content_id" 2>/dev/null)"
273
+ fi
274
+ [[ -n "$item" && -n "${STATUS_FIELD_ID:-}" && -n "${STATUS_OPT_IN_PROGRESS:-}" ]] \
275
+ && project_set_single_select "$item" "$STATUS_FIELD_ID" "$STATUS_OPT_IN_PROGRESS" >/dev/null 2>&1 \
276
+ && echo "[#$num] board → In Progress" >&2 || true
277
+ fi
278
+
279
+ echo "$wt|$branch|$num"
280
+ }