@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.
- package/.claude-plugin/plugin.json +22 -0
- package/AGENTS.md +101 -0
- package/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +21 -0
- package/README.md +143 -0
- package/SECURITY.md +22 -0
- package/bin/cckit +215 -0
- package/cckit.config.json +34 -0
- package/commands/kit-add.md +42 -0
- package/commands/kit-docs.md +45 -0
- package/commands/kit-doctor.md +52 -0
- package/commands/kit-export-project.md +58 -0
- package/commands/kit-export-training.md +49 -0
- package/commands/kit-init.md +126 -0
- package/commands/kit-routines.md +59 -0
- package/commands/kit-update.md +132 -0
- package/docs/kit-annotate/01-explainer.html +225 -0
- package/docs/kit-annotate/02-implementation-plan.html +196 -0
- package/docs/media/.onboarding-capture.cast +5 -0
- package/docs/media/README.md +43 -0
- package/docs/media/build-demo.sh +63 -0
- package/docs/media/build-kit-init.sh +51 -0
- package/docs/media/build-onboarding.sh +51 -0
- package/docs/media/kit-dry-run.cast +107 -0
- package/docs/media/kit-dry-run.gif +0 -0
- package/docs/media/kit-init.cast +56 -0
- package/docs/media/kit-init.gif +0 -0
- package/docs/media/kit-onboarding.cast +148 -0
- package/docs/media/kit-onboarding.gif +0 -0
- package/githooks/pre-commit +18 -0
- package/kit.config.schema.json +105 -0
- package/package.json +54 -0
- package/privacy-denylist.example +8 -0
- package/profiles/automation.json +36 -0
- package/profiles/content.json +41 -0
- package/profiles/minimal.json +31 -0
- package/profiles/research.json +37 -0
- package/profiles/software.json +32 -0
- package/scripts/annotate-setup.sh +149 -0
- package/scripts/autopilot.sh +50 -0
- package/scripts/capture-project-ids.sh +53 -0
- package/scripts/check.sh +66 -0
- package/scripts/contribute.sh +48 -0
- package/scripts/debug.sh +54 -0
- package/scripts/init-upgrade-test.sh +99 -0
- package/scripts/init.sh +827 -0
- package/scripts/install.sh +24 -0
- package/scripts/kit-add-test.sh +62 -0
- package/scripts/kit-add.sh +115 -0
- package/scripts/kit-adopt-test.sh +61 -0
- package/scripts/kit-adopt.sh +122 -0
- package/scripts/kit-bump-version.sh +79 -0
- package/scripts/kit-digest.sh +126 -0
- package/scripts/kit-doctor.sh +663 -0
- package/scripts/kit-export-project-test.sh +82 -0
- package/scripts/kit-export-project.sh +245 -0
- package/scripts/kit-export-training-test.sh +51 -0
- package/scripts/kit-export-training.sh +175 -0
- package/scripts/kit-migrate-test.sh +80 -0
- package/scripts/kit-migrate.sh +190 -0
- package/scripts/kit-onboard-test.sh +63 -0
- package/scripts/kit-onboard.sh +69 -0
- package/scripts/kit-promote-test.sh +54 -0
- package/scripts/kit-promote.sh +102 -0
- package/scripts/kit-remove-test.sh +61 -0
- package/scripts/kit-remove.sh +84 -0
- package/scripts/kit-routines.sh +322 -0
- package/scripts/kit-version-check.sh +91 -0
- package/scripts/kit-wire-test.sh +54 -0
- package/scripts/kit-wire.sh +132 -0
- package/scripts/knowledge-lint.sh +96 -0
- package/scripts/lib/cckit-output.sh +36 -0
- package/scripts/lib/effort-metrics.sh +452 -0
- package/scripts/lib/effort-ops-test.sh +83 -0
- package/scripts/lib/effort-ops.sh +132 -0
- package/scripts/lib/effort-plan.sh +104 -0
- package/scripts/lib/effort.sh +191 -0
- package/scripts/lib/engine-adapter.sh +92 -0
- package/scripts/lib/gh-log.sh +58 -0
- package/scripts/lib/gh-project.sh +212 -0
- package/scripts/lib/handoff.sh +35 -0
- package/scripts/lib/kit-cli-test.sh +42 -0
- package/scripts/lib/kit-cli.sh +32 -0
- package/scripts/lib/kit-config-resolve.sh +145 -0
- package/scripts/lib/kit-config.sh +88 -0
- package/scripts/lib/kit-engine-test.sh +107 -0
- package/scripts/lib/kit-events.sh +62 -0
- package/scripts/lib/kit-gc.sh +117 -0
- package/scripts/lib/kit-interview-test.sh +77 -0
- package/scripts/lib/kit-interview.sh +203 -0
- package/scripts/lib/kit-local.sh +79 -0
- package/scripts/lib/kit-manifest.sh +127 -0
- package/scripts/lib/kit-mode-test.sh +49 -0
- package/scripts/lib/kit-mode.sh +67 -0
- package/scripts/lib/kit-operate.sh +105 -0
- package/scripts/lib/kit-profile-test.sh +62 -0
- package/scripts/lib/kit-profile.sh +115 -0
- package/scripts/lib/kit-task-ops-test.sh +63 -0
- package/scripts/lib/kit-task-ops.sh +341 -0
- package/scripts/lib/pr-evidence.sh +173 -0
- package/scripts/lib/project-scan.sh +16 -0
- package/scripts/lib/react-detect.sh +78 -0
- package/scripts/lib/role-identity.sh +47 -0
- package/scripts/lib/secret-guard.sh +96 -0
- package/scripts/lib/toon.sh +35 -0
- package/scripts/lib/ui.sh +42 -0
- package/scripts/lib/version-bump.sh +59 -0
- package/scripts/lib/worktree-issue-test.sh +45 -0
- package/scripts/lib/worktree-issue.sh +73 -0
- package/scripts/lib/worktree-start.sh +280 -0
- package/scripts/orchestrate.sh +160 -0
- package/scripts/portable-test.sh +53 -0
- package/scripts/publish.sh +94 -0
- package/scripts/setup-labels.sh +25 -0
- package/scripts/setup-milestones.sh +17 -0
- package/scripts/showcase.sh +64 -0
- package/scripts/status.sh +44 -0
- package/scripts/task-sync.sh +59 -0
- package/scripts/test.sh +48 -0
- package/scripts/web-install.sh +22 -0
- package/skills/kit-annotate/SKILL.md +107 -0
- package/skills/kit-autopilot/SKILL.md +108 -0
- package/skills/kit-contribute/SKILL.md +134 -0
- package/skills/kit-customize/SKILL.md +134 -0
- package/skills/kit-dev/SKILL.md +67 -0
- package/skills/kit-digest/SKILL.md +41 -0
- package/skills/kit-effort-close/SKILL.md +156 -0
- package/skills/kit-effort-new/SKILL.md +173 -0
- package/skills/kit-effort-pr/SKILL.md +139 -0
- package/skills/kit-effort-start/SKILL.md +85 -0
- package/skills/kit-gc/SKILL.md +80 -0
- package/skills/kit-onboard/SKILL.md +50 -0
- package/skills/kit-security-sweep/SKILL.md +57 -0
- package/skills/kit-ship/SKILL.md +43 -0
- package/skills/kit-task-close/SKILL.md +66 -0
- package/skills/kit-task-new/SKILL.md +51 -0
- package/skills/kit-task-pr/SKILL.md +43 -0
- package/skills/kit-task-pr-auto/SKILL.md +27 -0
- package/skills/kit-task-pr-merge/SKILL.md +53 -0
- package/skills/kit-task-start/SKILL.md +76 -0
- package/skills/kit-task-sync/SKILL.md +37 -0
- package/templates/CLAUDE.md.tmpl +106 -0
- package/templates/agents/analyst.md +55 -0
- package/templates/agents/auto-dev.md +93 -0
- package/templates/agents/backend.md +59 -0
- package/templates/agents/designer.md +73 -0
- package/templates/agents/devops.md +57 -0
- package/templates/agents/editor.md +48 -0
- package/templates/agents/frontend.md +81 -0
- package/templates/agents/generalist.md +46 -0
- package/templates/agents/local-delegate.md +70 -0
- package/templates/agents/n8n.md +65 -0
- package/templates/agents/pm.md +69 -0
- package/templates/agents/qa.md +66 -0
- package/templates/agents/researcher.md +57 -0
- package/templates/agents/security.md +65 -0
- package/templates/agents/tech-lead.md +75 -0
- package/templates/hooks/guard-base-branch-commit.sh.tmpl +45 -0
- package/templates/hooks/kit-local-status.sh.tmpl +34 -0
- package/templates/hooks/kit_version_check.sh.tmpl +6 -0
- package/templates/hooks/mempal_followup.sh.tmpl +97 -0
- package/templates/hooks/mempal_precompact.sh.tmpl +4 -0
- package/templates/hooks/mempal_save.sh.tmpl +4 -0
- package/templates/hooks/mempal_session_start.sh.tmpl +8 -0
- package/templates/hooks/prepush_gate.sh.tmpl +36 -0
- package/templates/hooks/repo-hygiene.sh.tmpl +72 -0
- package/templates/kit.config.json.tmpl +32 -0
- package/templates/knowledge-INDEX.md.tmpl +12 -0
- package/templates/lib/kit-sigil.sh.tmpl +124 -0
- package/templates/rules/branch-naming.md +104 -0
- package/templates/rules/communication-style.md +22 -0
- package/templates/rules/delegation-brief.md +40 -0
- package/templates/rules/design-routing.md +35 -0
- package/templates/rules/effort-model.md +122 -0
- package/templates/rules/knowledge-base.md +41 -0
- package/templates/rules/mempalace.md +110 -0
- package/templates/rules/plan-output-format.md +58 -0
- package/templates/rules/react-annotate.md +69 -0
- package/templates/rules/risk-tiered-review.md +62 -0
- package/templates/rules/skill-gaps.md +48 -0
- package/templates/rules/task-management.md +42 -0
- package/templates/settings/settings.local.json.tmpl +27 -0
- package/templates/skills/NAMESPACED +13 -0
- package/templates/skills/copywriting/SKILL.md +252 -0
- package/templates/skills/copywriting/references/copy-frameworks.md +344 -0
- package/templates/skills/copywriting/references/natural-transitions.md +272 -0
- package/templates/skills/feature-build-refine/SKILL.md +367 -0
- package/templates/skills/karpathy-guidelines/SKILL.md +69 -0
- package/templates/skills/morning-briefing/SKILL.md +46 -0
- package/templates/skills/speckit/SKILL.md +239 -0
- 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
|
+
}
|