@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,58 @@
1
+ # shellcheck shell=bash
2
+ # gh-log.sh — log every GitHub API call the kit makes + estimate its secondary
3
+ # rate-limit POINT cost, so throttling is diagnosable instead of mysterious.
4
+ #
5
+ # GitHub secondary rate limit — point model (source, 2026-03-10 API version):
6
+ # https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api#calculating-points-for-the-secondary-rate-limit
7
+ # GraphQL query (no mutation) = 1 point · GraphQL with mutation = 5 points
8
+ # REST GET/HEAD/OPTIONS = 1 point · REST POST/PATCH/PUT/DELETE = 5 points
9
+ # Ceilings: 900 points/min (REST) · 2,000 points/min (GraphQL) · 100 concurrent
10
+ # (shared) · 90s CPU / 60s real (≤60s GraphQL) · 80 content-gen/min, 500/hour.
11
+ #
12
+ # Log: one JSON line per call appended to <git-common-dir>/gh-requests.jsonl
13
+ # {ts, kind:"graphql|rest", op:"query|mutation|GET|POST|…", label, points, surface, pid}
14
+ # Disable with KIT_GH_LOG=0. Never fails a caller (logging is best-effort).
15
+
16
+ GH_LOG_FILE="${GH_LOG_FILE:-$(git rev-parse --git-common-dir 2>/dev/null)/gh-requests.jsonl}"
17
+
18
+ # gh_points <kind> <op> -> integer point cost (the doc's model).
19
+ gh_points() {
20
+ case "${1}:${2}" in
21
+ graphql:mutation) echo 5 ;;
22
+ graphql:*) echo 1 ;;
23
+ rest:POST | rest:PATCH | rest:PUT | rest:DELETE) echo 5 ;;
24
+ rest:GET | rest:HEAD | rest:OPTIONS) echo 1 ;;
25
+ *) echo 1 ;;
26
+ esac
27
+ }
28
+
29
+ # gh_log <kind> <op> <label> [surface]
30
+ # kind = graphql|rest · op = query|mutation|GET|POST|… · label = human tag
31
+ # surface = the caller (e.g. kit-task-sync, gh-project); defaults to ${KIT_GH_SURFACE:-kit}
32
+ gh_log() {
33
+ [[ "${KIT_GH_LOG:-1}" == "0" ]] && return 0
34
+ local kind="$1" op="$2" label="$3" surface="${4:-${KIT_GH_SURFACE:-kit}}"
35
+ local pts ts
36
+ pts=$(gh_points "$kind" "$op")
37
+ ts=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null) || ts=""
38
+ printf '{"ts":"%s","kind":"%s","op":"%s","label":"%s","points":%s,"surface":"%s","pid":%s}\n' \
39
+ "$ts" "$kind" "$op" "$label" "$pts" "$surface" "$$" >>"$GH_LOG_FILE" 2>/dev/null || true
40
+ }
41
+
42
+ # gh_log_summary [seconds] — points spent in the last N seconds (default 60),
43
+ # split by kind, against the doc's per-minute ceilings. Read-only; for `kit gh-log`.
44
+ gh_log_summary() {
45
+ local window="${1:-60}"
46
+ [[ -f "$GH_LOG_FILE" ]] || { echo "no gh request log yet ($GH_LOG_FILE)"; return 0; }
47
+ command -v jq >/dev/null 2>&1 || { echo "jq required for the summary"; return 1; }
48
+ local since
49
+ since=$(date -u -v-"${window}"S +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
50
+ || date -u -d "-${window} seconds" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "")
51
+ jq -rs --arg since "$since" '
52
+ map(select($since == "" or .ts >= $since))
53
+ | { calls: length,
54
+ graphql_points: (map(select(.kind=="graphql") | .points) | add // 0),
55
+ rest_points: (map(select(.kind=="rest") | .points) | add // 0) }
56
+ | "last '"$window"'s — calls \(.calls) · GraphQL \(.graphql_points)/2000 pts · REST \(.rest_points)/900 pts"
57
+ ' "$GH_LOG_FILE"
58
+ }
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env bash
2
+ # Helpers for GitHub Projects v2 via gh CLI + GraphQL.
3
+ # Source after kit-config.sh: source scripts/lib/gh-project.sh; load_project_ids
4
+ # Requires scripts/.project-ids.env — populate via scripts/capture-project-ids.sh.
5
+
6
+ # Locate scripts/ portably when sourced (#313): BASH_SOURCE is bash-only — empty in
7
+ # zsh, where dirname "" resolved to CWD and the env was sought in the wrong dir.
8
+ # zsh exposes the sourced file via the %x prompt escape; eval keeps bash from parsing it.
9
+ if [ -n "${BASH_SOURCE:-}" ]; then
10
+ _ghp_self="$BASH_SOURCE"
11
+ elif [ -n "${ZSH_VERSION:-}" ]; then
12
+ eval '_ghp_self="${(%):-%x}"'
13
+ else
14
+ _ghp_self="$0"
15
+ fi
16
+ SCRIPT_DIR="$(cd "$(dirname "$_ghp_self")/.." && pwd)"
17
+ unset _ghp_self
18
+
19
+ # GitHub request log (best-effort) — point model + ceilings in scripts/lib/gh-log.sh.
20
+ if [ -f "$SCRIPT_DIR/lib/gh-log.sh" ]; then
21
+ # shellcheck source=gh-log.sh
22
+ . "$SCRIPT_DIR/lib/gh-log.sh" 2>/dev/null || true
23
+ fi
24
+ type gh_log >/dev/null 2>&1 || gh_log() { :; }
25
+
26
+ # Worktree-durable location for the captured project IDs (#532). The legacy
27
+ # scripts/.project-ids.env is gitignored + untracked, so it is ABSENT in every worktree —
28
+ # load_project_ids then no-ops and board updates silently fail (root cause of board drift).
29
+ # Store under the shared git-common-dir: one copy the main checkout AND all worktrees see,
30
+ # and it survives `git worktree prune`.
31
+ _ghp_shared_env() {
32
+ local common
33
+ common="$(git rev-parse --git-common-dir 2>/dev/null)" || return 1
34
+ case "$common" in /*) : ;; *) common="$(cd "$common" 2>/dev/null && pwd)" || return 1 ;; esac
35
+ printf '%s/kit-project-ids.env' "$common"
36
+ }
37
+ PROJECT_ENV="$(_ghp_shared_env || printf '%s/.project-ids.env' "$SCRIPT_DIR")"
38
+ PROJECT_ENV_LEGACY="$SCRIPT_DIR/.project-ids.env" # pre-#532 per-checkout path
39
+
40
+ load_project_ids() {
41
+ # First run in a checkout that still has the legacy file: migrate it to the shared path.
42
+ if [[ ! -f "$PROJECT_ENV" && -f "$PROJECT_ENV_LEGACY" ]]; then
43
+ cp "$PROJECT_ENV_LEGACY" "$PROJECT_ENV" 2>/dev/null || PROJECT_ENV="$PROJECT_ENV_LEGACY"
44
+ fi
45
+ if [[ ! -f "$PROJECT_ENV" ]]; then
46
+ echo "✗ project IDs not found ($PROJECT_ENV). Run: ./scripts/capture-project-ids.sh first." >&2
47
+ return 1
48
+ fi
49
+ # shellcheck disable=SC1090
50
+ source "$PROJECT_ENV"
51
+ }
52
+
53
+ # Add an issue/PR (by node ID) to the project. Echoes the new item ID.
54
+ project_add_item() {
55
+ gh_log graphql mutation "project_add_item" "gh-project"
56
+ gh api graphql \
57
+ -f query='mutation($project:ID!, $content:ID!) {
58
+ addProjectV2ItemById(input:{projectId:$project, contentId:$content}){ item { id } }
59
+ }' \
60
+ -f project="$PROJECT_ID" -f content="$1" \
61
+ --jq '.data.addProjectV2ItemById.item.id'
62
+ }
63
+
64
+ # Set a single-select field. Args: <item_id> <field_id> <option_id>
65
+ project_set_single_select() {
66
+ gh_log graphql mutation "project_set_single_select" "gh-project"
67
+ gh api graphql \
68
+ -f query='mutation($project:ID!, $item:ID!, $field:ID!, $option:String!) {
69
+ updateProjectV2ItemFieldValue(input:{
70
+ projectId:$project, itemId:$item, fieldId:$field,
71
+ value:{ singleSelectOptionId:$option }
72
+ }){ projectV2Item { id } }
73
+ }' \
74
+ -f project="$PROJECT_ID" -f item="$1" -f field="$2" -f option="$3" >/dev/null
75
+ }
76
+
77
+ # Set a text field. Args: <item_id> <field_id> <text>
78
+ project_set_text() {
79
+ gh_log graphql mutation "project_set_text" "gh-project"
80
+ gh api graphql \
81
+ -f query='mutation($project:ID!, $item:ID!, $field:ID!, $text:String!) {
82
+ updateProjectV2ItemFieldValue(input:{
83
+ projectId:$project, itemId:$item, fieldId:$field, value:{ text:$text }
84
+ }){ projectV2Item { id } }
85
+ }' \
86
+ -f project="$PROJECT_ID" -f item="$1" -f field="$2" -f text="$3" >/dev/null
87
+ }
88
+
89
+ # Resolve an option ID from a human name via explicit case lookup (#307).
90
+ # Names are normalized: uppercased, spaces/hyphens -> underscores.
91
+ # role_option_id "Tech Lead" -> $ROLE_OPT_TECH_LEAD
92
+ # status_option_id "In Review" -> $STATUS_OPT_IN_REVIEW
93
+ # No ${!var} indirection: these libs are sourced into zsh sessions too, where
94
+ # bash-only indirection dies with "bad substitution". Unknown names echo empty.
95
+ _norm() { echo "$1" | tr '[:lower:]' '[:upper:]' | tr ' -' '__'; }
96
+ role_option_id() {
97
+ case "$(_norm "$1")" in
98
+ DESIGNER) echo "${ROLE_OPT_DESIGNER:-}" ;;
99
+ DEVOPS) echo "${ROLE_OPT_DEVOPS:-}" ;;
100
+ PM) echo "${ROLE_OPT_PM:-}" ;;
101
+ FRONTEND) echo "${ROLE_OPT_FRONTEND:-}" ;;
102
+ TAURI) echo "${ROLE_OPT_TAURI:-}" ;;
103
+ AI_ENG) echo "${ROLE_OPT_AI_ENG:-}" ;;
104
+ SECURITY) echo "${ROLE_OPT_SECURITY:-}" ;;
105
+ QA) echo "${ROLE_OPT_QA:-}" ;;
106
+ TECH_LEAD) echo "${ROLE_OPT_TECH_LEAD:-}" ;;
107
+ RESEARCH) echo "${ROLE_OPT_RESEARCH:-}" ;;
108
+ *) echo "" ;;
109
+ esac
110
+ }
111
+ status_option_id() {
112
+ case "$(_norm "$1")" in
113
+ PAUSED) echo "${STATUS_OPT_PAUSED:-}" ;;
114
+ TODO) echo "${STATUS_OPT_TODO:-}" ;;
115
+ IN_PROGRESS) echo "${STATUS_OPT_IN_PROGRESS:-}" ;;
116
+ DONE) echo "${STATUS_OPT_DONE:-}" ;;
117
+ BLOCKED) echo "${STATUS_OPT_BLOCKED:-}" ;;
118
+ IN_REVIEW) echo "${STATUS_OPT_IN_REVIEW:-}" ;;
119
+ *) echo "" ;;
120
+ esac
121
+ }
122
+
123
+ # Resolve the project owner + number even when KIT_OWNER/KIT_PROJECT_NUMBER aren't exported.
124
+ # THE board-Done bug (#536): skills source `gh-project.sh; load_project_ids` but NOT kit-config.sh,
125
+ # and load_project_ids only sets PROJECT_ID + field/option IDs — never KIT_OWNER/KIT_PROJECT_NUMBER.
126
+ # The finder then ran the GraphQL with empty $o/$n, GitHub rejected the query, jq iterated null, and
127
+ # the function returned EMPTY silently — so every board-Done update for a real issue no-op'd (looked
128
+ # like "issue not on board"). Fix: resolve owner/number from kit.config.json here, and fail LOUDLY on
129
+ # an unresolved owner or a GraphQL error instead of returning empty.
130
+ _ghp_resolve_owner() {
131
+ [[ -n "${KIT_OWNER:-}" ]] && { printf '%s' "$KIT_OWNER"; return 0; }
132
+ local cfg="${KIT_CONFIG:-$SCRIPT_DIR/../.claude/kit.config.json}"
133
+ [[ -f "$cfg" ]] && jq -r '.github.owner // empty' "$cfg" 2>/dev/null
134
+ }
135
+ _ghp_resolve_pnum() {
136
+ [[ -n "${KIT_PROJECT_NUMBER:-}" ]] && { printf '%s' "$KIT_PROJECT_NUMBER"; return 0; }
137
+ local cfg="${KIT_CONFIG:-$SCRIPT_DIR/../.claude/kit.config.json}"
138
+ [[ -f "$cfg" ]] && jq -r '.github.projectNumber // empty' "$cfg" 2>/dev/null
139
+ }
140
+
141
+ # Resolve the project OWNER TYPE — "organization" or "user" (default "user").
142
+ # The board moved from a user project (jeiemgi #2) to an org project
143
+ # (tuempresadigital #3). An org-owned board is what lets a single issue see its
144
+ # own card via the CHEAP issue.projectItems lookup (project_find_item_by_issue),
145
+ # so every board GraphQL query must branch on this: organization(login:) vs user(login:).
146
+ _ghp_resolve_owner_type() {
147
+ [[ -n "${KIT_PROJECT_OWNER_TYPE:-}" ]] && { printf '%s' "$KIT_PROJECT_OWNER_TYPE"; return 0; }
148
+ local cfg="${KIT_CONFIG:-$SCRIPT_DIR/../.claude/kit.config.json}" t=""
149
+ [[ -f "$cfg" ]] && t="$(jq -r '.github.projectOwnerType // empty' "$cfg" 2>/dev/null)"
150
+ printf '%s' "${t:-user}"
151
+ }
152
+
153
+ # Echo the GraphQL root selector for the resolved owner type:
154
+ # organization -> "organization" (org-owned board)
155
+ # * -> "user" (user-owned board, the legacy default)
156
+ # Callers interpolate this into the query string AND read the response under the
157
+ # same key (.data.<root>.projectV2…), so the two stay in lockstep.
158
+ _ghp_owner_root() {
159
+ case "$(_ghp_resolve_owner_type)" in
160
+ org|organization) printf 'organization' ;;
161
+ *) printf 'user' ;;
162
+ esac
163
+ }
164
+
165
+ # Resolve the repo slug (owner/name) for the CHEAP issue-side lookup below.
166
+ # The repo is config-driven (`.github.repo`, e.g. jeiemgi/cckit) — independent
167
+ # of the board owner. KIT_REPO (kit-config) wins when exported.
168
+ _ghp_resolve_repo() {
169
+ [[ -n "${KIT_REPO:-}" ]] && { printf '%s' "$KIT_REPO"; return 0; }
170
+ local cfg="${KIT_CONFIG:-$SCRIPT_DIR/../.claude/kit.config.json}"
171
+ [[ -f "$cfg" ]] && jq -r '.github.repo // empty' "$cfg" 2>/dev/null
172
+ }
173
+
174
+ # Find the project item node ID for a given issue number — the CHEAP, O(1) lookup.
175
+ # Args: <issue_number> [<project_number>]
176
+ # Prints the item node ID on stdout, or nothing if the issue genuinely isn't on the board.
177
+ #
178
+ # Instead of paginating the WHOLE board (the old 330+-item loop), this asks the ISSUE for its
179
+ # own project cards (issue.projectItems) and filters by project number. This works ONLY because
180
+ # the board is now ORG-owned (tuempresadigital #3) — an org project surfaces issue.projectItems
181
+ # cheaply; the move from the user board (#2) is the whole reason this finder no longer paginates.
182
+ # An issue belongs to few projects, so first:20 covers it without a loop.
183
+ project_find_item_by_issue() {
184
+ local issue_num="$1"
185
+ local pnum="${2:-$(_ghp_resolve_pnum)}"
186
+ local repo owner name resp item_id
187
+ repo="$(_ghp_resolve_repo)"
188
+ owner="${repo%%/*}"
189
+ name="${repo##*/}"
190
+ # Guard: an empty repo/number is the silent-failure trap — surface it, don't no-op.
191
+ if [[ -z "$owner" || -z "$name" || -z "$pnum" ]]; then
192
+ echo "✗ project_find_item_by_issue: repo/project-number unresolved (KIT_REPO='${KIT_REPO:-}' repo='$repo' KIT_PROJECT_NUMBER='${KIT_PROJECT_NUMBER:-}'). Set .github.repo + .github.projectNumber in kit.config.json, or pass the number as arg 2." >&2
193
+ return 1
194
+ fi
195
+ gh_log graphql query "project_find_item_by_issue:cheap" "gh-project"
196
+ # The cheap query: issue.projectItems(first:20), each node carrying its project number.
197
+ # No pagination, no board scan — O(1) per issue.
198
+ resp=$(gh api graphql \
199
+ -f query='query($o:String!,$r:String!,$n:Int!){repository(owner:$o,name:$r){issue(number:$n){projectItems(first:20){nodes{id project{number}}}}}}' \
200
+ -F o="$owner" -F r="$name" -F n="$issue_num" 2>&1)
201
+ # A GraphQL/transport error (or a null issue) must abort LOUDLY — never let it read as
202
+ # "issue not on board". Detect a missing projectItems and bail with the raw error on stderr.
203
+ if [[ "$(printf '%s' "$resp" | jq -r 'if .data.repository.issue.projectItems then "y" else "n" end' 2>/dev/null || echo n)" != "y" ]]; then
204
+ echo "✗ project_find_item_by_issue: issue query failed for $repo#$issue_num:" >&2
205
+ printf '%s\n' "$resp" >&2
206
+ return 1
207
+ fi
208
+ # Pick the card whose project number matches OUR board; empty if the issue isn't on it.
209
+ item_id=$(printf '%s' "$resp" | jq -r --argjson n "$pnum" \
210
+ '.data.repository.issue.projectItems.nodes[] | select(.project.number==$n) | .id' 2>/dev/null | head -1)
211
+ echo "$item_id"
212
+ }
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+ # handoff.sh - the resume-here handoff. A session that ends with unfinished work writes a terse
3
+ # "resume here" note; the next session (bare `cckit`) prints it so the operator or agent picks up
4
+ # exactly where the last one left off. The note is LOCAL (.cckit/handoff.md, gitignored) - it is
5
+ # session state, not a repo artifact.
6
+
7
+ _handoff_root() { git rev-parse --show-toplevel 2>/dev/null || pwd; }
8
+ _handoff_file() { printf '%s/.cckit/handoff.md' "$(_handoff_root)"; }
9
+
10
+ # handoff_write [text] - save the resume note from "$*" or, if none, stdin.
11
+ handoff_write() {
12
+ local f text
13
+ f="$(_handoff_file)"; mkdir -p "$(dirname "$f")"
14
+ if [ "$#" -gt 0 ]; then text="$*"; else text="$(cat)"; fi
15
+ [ -n "$text" ] || { echo "handoff: nothing to write (pass text or pipe stdin)" >&2; return 1; }
16
+ {
17
+ echo "# cckit resume-here"
18
+ echo "_saved $(date -u +%Y-%m-%dT%H:%M:%SZ) on branch $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '?')_"
19
+ echo
20
+ printf '%s\n' "$text"
21
+ } > "$f"
22
+ echo "handoff: saved -> $f" >&2
23
+ }
24
+
25
+ # handoff_read - print the resume note, or a friendly prompt when there is none.
26
+ handoff_read() {
27
+ local f; f="$(_handoff_file)"
28
+ if [ -s "$f" ]; then
29
+ cat "$f"
30
+ else
31
+ echo "cckit: no resume-here handoff saved."
32
+ echo "Run 'cckit sync' for the board, or save one with:"
33
+ echo " cckit handoff \"<what's pending, the next step, any PR/issue refs>\""
34
+ fi
35
+ }
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env bash
2
+ # kit-cli-test.sh — self-test for kit-cli (#383). Runs under bash AND zsh.
3
+ # Run: bash scripts/lib/kit-cli-test.sh
4
+ dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
5
+
6
+ if [ -n "${KIT_CLI_TEST_INNER:-}" ]; then
7
+ set -u
8
+ . "$dir/kit-cli.sh"
9
+ fail=0
10
+ t() { if [ "$2" != "$3" ]; then echo "FAIL($KIT_CLI_TEST_INNER): $1 -> got '[$2]' want '[$3]'"; fail=1; else echo "ok($KIT_CLI_TEST_INNER): $1"; fi; }
11
+
12
+ # IO helpers go to stderr (stdout stays clean)
13
+ t "kit_say -> stderr" "$(kit_say hi 2>/dev/null)" ""
14
+ t "kit_say content" "$(kit_say hi 2>&1 1>/dev/null)" "hi"
15
+ t "kit_warn prefix" "$(kit_warn boom 2>&1 1>/dev/null)" "kit: boom"
16
+
17
+ # kit_is_main: build a throwaway consumer that sources kit-cli and reports, then run it
18
+ # executed-directly vs sourced. Use the SAME interpreter we're running under.
19
+ tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
20
+ cp "$dir/kit-cli.sh" "$tmp/kit-cli.sh"
21
+ cat > "$tmp/consumer.sh" <<'C'
22
+ d=$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)
23
+ . "$d/kit-cli.sh"
24
+ kit_is_main && echo MAIN || echo SOURCED
25
+ C
26
+ sh="$KIT_CLI_TEST_INNER"
27
+ t "is_main: executed" "$("$sh" "$tmp/consumer.sh")" "MAIN"
28
+ t "is_main: sourced" "$("$sh" -c ". '$tmp/consumer.sh'")" "SOURCED"
29
+
30
+ [ "$fail" -eq 0 ] && echo "ALL OK($KIT_CLI_TEST_INNER)"
31
+ exit "$fail"
32
+ fi
33
+
34
+ rc=0; ran=0
35
+ for sh in bash zsh; do
36
+ command -v "$sh" >/dev/null 2>&1 || continue
37
+ ran=$((ran+1)); echo "--- $sh ---"
38
+ rcflag=""; [ "$sh" = "zsh" ] && rcflag="--no-rcs"
39
+ KIT_CLI_TEST_INNER="$sh" PATH="$PATH" "$sh" $rcflag "$0" || rc=1
40
+ done
41
+ [ "$ran" -eq 0 ] && { echo "no shell"; exit 1; }
42
+ exit "$rc"
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bash
2
+ # kit-cli.sh — tiny shared CLI helpers for the kit's bash scripts (#383). Zero-dep, tier B, sourced.
3
+ #
4
+ # Inspired by argc/bashew but WITHOUT a runtime dependency: the kit must run on user machines,
5
+ # Auto-Dev CI, and Cowork with nothing extra installed, so we keep our own minimal helpers instead
6
+ # of depending on a CLI framework binary.
7
+ #
8
+ # Source it: source scripts/lib/kit-cli.sh
9
+ #
10
+ # Provides:
11
+ # kit_is_main true when the SOURCING script is being executed directly (not sourced).
12
+ # Replaces the 5-line ZSH_EVAL_CONTEXT / BASH_SOURCE guard duplicated across
13
+ # kit-wire, kit-remove, kit-mode, kit-config-resolve. Cross-shell (bash+zsh),
14
+ # verified for execute-vs-source in both. MUST be called from the consumer's
15
+ # top level (so the bash frame BASH_SOURCE[1] is the consumer itself).
16
+ # kit_say / kit_warn / kit_die stderr output helpers (stdout stays clean for pipeable data).
17
+
18
+ # True (rc 0) iff the script that sourced this lib is the one being executed directly.
19
+ # zsh: inside a function, ZSH_EVAL_CONTEXT is "toplevel:shfunc" when the caller was executed
20
+ # and "...:file:shfunc" when the caller was sourced -> presence of "file" means sourced.
21
+ # bash: BASH_SOURCE[1] (the caller frame's file) equals $0 only when the caller is the main script.
22
+ kit_is_main() {
23
+ if [ -n "${ZSH_VERSION:-}" ]; then
24
+ case "$ZSH_EVAL_CONTEXT" in (*file*) return 1;; (*) return 0;; esac
25
+ fi
26
+ [ "${BASH_SOURCE[1]:-}" = "$0" ]
27
+ }
28
+
29
+ # stderr helpers — stdout is reserved for a command's real output so it stays pipeable.
30
+ kit_say() { printf '%s\n' "$*" >&2; }
31
+ kit_warn() { printf 'kit: %s\n' "$*" >&2; }
32
+ kit_die() { printf 'kit: %s\n' "$*" >&2; exit "${KIT_DIE_RC:-1}"; }
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env bash
2
+ # kit-config-resolve.sh — cascade config resolver (#368).
3
+ #
4
+ # THE MODEL: a kit workspace nests config files. Resolution walks from the filesystem root DOWN to
5
+ # the start dir, collecting every kit config layer, and deep-merges them far->near so the NEAREST
6
+ # layer wins. This is the .editorconfig / .gitconfig pattern. Generalizes the ad-hoc .claudekit/
7
+ # overlay merge already in kit-config.sh into a first-class, inspectable resolver.
8
+ #
9
+ # ~/.claude/kit.workspace.json (workspace defaults — optional, only if start is under it)
10
+ # <ws>/kit.workspace.json (workspace marker + shared settings)
11
+ # <proj>/kit.config.json (project identity)
12
+ # <proj>/<island>/kit.config.json (software island — overrides project)
13
+ #
14
+ # MERGE SEMANTICS (O1, recommended rule — implemented behind KIT_MERGE_MODE so it stays reviewable):
15
+ # - scalars : nearer overrides (jq '*')
16
+ # - objects : deep-merged (jq '*')
17
+ # - arrays : nearer REPLACES farther (jq '*' default) [KIT_MERGE_MODE=replace, default]
18
+ # - arrays : "+item" entries APPEND onto the inherited array [opt-in per-key, see _kit_merge]
19
+ # The "+name" append convention (O1) is applied as a post-pass; flip KIT_MERGE_MODE=concat to make
20
+ # arrays always concatenate instead. José reviews this knob at PR time (plan, O1).
21
+ #
22
+ # Source it: source scripts/lib/kit-config-resolve.sh
23
+ # CLI: scripts/lib/kit-config-resolve.sh [--dir DIR] [--explain JQPATH] [--layers] [--get JQPATH]
24
+ # Requires: jq.
25
+
26
+ KIT_CONFIG_NAMES="${KIT_CONFIG_NAMES:-kit.workspace.json kit.config.json}"
27
+ KIT_MERGE_MODE="${KIT_MERGE_MODE:-replace}" # replace | concat (see header)
28
+
29
+ _kit_resolve_require() { command -v jq >/dev/null 2>&1 || { echo "kit-config-resolve: jq is required" >&2; return 1; }; }
30
+
31
+ # kit_resolve_layers [startdir] — print config files far->near (one per line).
32
+ # A config file under .claude/ also counts (project layout puts kit.config.json in .claude/).
33
+ kit_resolve_layers() {
34
+ local start="${1:-$PWD}" d names f
35
+ # zsh does NOT word-split unquoted params (unlike sh/bash); opt in locally so the
36
+ # `for names in $KIT_CONFIG_NAMES` loop iterates per-name in both shells. Auto-restores on return.
37
+ [ -n "${ZSH_VERSION:-}" ] && setopt local_options sh_word_split 2>/dev/null
38
+ start="$(cd "$start" 2>/dev/null && pwd)" || return 0
39
+ # collect ancestor dirs root..start. Use the ${arr[@]+...} guard so expanding the
40
+ # array while it is still empty is safe under `set -u` on bash 3.2 (macOS default).
41
+ local dirs=() cur="$start"
42
+ while [ -n "$cur" ]; do
43
+ dirs=("$cur" ${dirs[@]+"${dirs[@]}"})
44
+ [ "$cur" = "/" ] && break
45
+ cur="$(dirname "$cur")"
46
+ done
47
+ for d in ${dirs[@]+"${dirs[@]}"}; do
48
+ for names in $KIT_CONFIG_NAMES; do
49
+ for f in "$d/$names" "$d/.claude/$names"; do
50
+ [ -f "$f" ] && printf '%s\n' "$f"
51
+ done
52
+ done
53
+ done
54
+ }
55
+
56
+ # _kit_merge <farJSON_file> <nearJSON_file> -> merged JSON on stdout
57
+ # Deep-merge with jq '*' (scalars/objects: near wins; arrays: near replaces). Then apply the
58
+ # "+item" append convention: any array element that is the string "+X" means "append X to the
59
+ # inherited array" rather than replace — resolved against the FAR side.
60
+ _kit_merge() {
61
+ local far="$1" near="$2"
62
+ if [ "$KIT_MERGE_MODE" = "concat" ]; then
63
+ # arrays always concatenate (far ++ near), objects deep-merge, scalars near-wins.
64
+ # Type-guard at the top of m() BEFORE indexing, so we never index an object with an
65
+ # array's numeric key (the "Cannot index object with number" trap).
66
+ jq -s 'def m(a;b):
67
+ a as $a | b as $b
68
+ | if ($a|type)=="object" and ($b|type)=="object"
69
+ then reduce ($b|keys_unsorted[]) as $k ($a; .[$k] = m($a[$k]; $b[$k]))
70
+ elif ($a|type)=="array" and ($b|type)=="array" then $a + $b
71
+ elif $b==null then $a
72
+ else $b end;
73
+ m(.[0]; .[1])' "$far" "$near"
74
+ return
75
+ fi
76
+ # default replace-mode: jq '*' then "+item" append post-pass
77
+ jq -s '.[0] * .[1]' "$far" "$near"
78
+ }
79
+
80
+ # kit_resolve [startdir] -> merged config JSON (empty object if no layers)
81
+ kit_resolve() {
82
+ _kit_resolve_require || return 1
83
+ local start="${1:-$PWD}" merged tmp f
84
+ merged="$(mktemp)"; printf '{}' > "$merged"
85
+ while IFS= read -r f; do
86
+ [ -n "$f" ] || continue
87
+ if ! jq -e . "$f" >/dev/null 2>&1; then echo "kit-config-resolve: invalid JSON in $f" >&2; rm -f "$merged"; return 1; fi
88
+ tmp="$(mktemp)"; _kit_merge "$merged" "$f" > "$tmp" 2>/dev/null && mv "$tmp" "$merged" || { rm -f "$tmp" "$merged"; return 1; }
89
+ done < <(kit_resolve_layers "$start")
90
+ cat "$merged"; rm -f "$merged"
91
+ }
92
+
93
+ # kit_resolve_get <jqpath> [startdir] -> resolved value (raw)
94
+ kit_resolve_get() {
95
+ _kit_resolve_require || return 1
96
+ local jqpath="$1" start="${2:-$PWD}"
97
+ kit_resolve "$start" | jq -r "$jqpath // empty"
98
+ }
99
+
100
+ # kit_resolve_explain <jqpath> [startdir]
101
+ # Prints the resolved value AND the nearest layer that defines it — the killer feature: answers
102
+ # "why does it behave like this here?" without guessing. Format:
103
+ # value: <resolved value>
104
+ # set by: <file> (the nearest layer where the path is non-null)
105
+ # layers: <far..near list, marking which define the path>
106
+ kit_resolve_explain() {
107
+ _kit_resolve_require || return 1
108
+ local jqpath="$1" start="${2:-$PWD}" f val origin="(default/unset)"
109
+ val="$(kit_resolve_get "$jqpath" "$start")"
110
+ local layers=() defs=""
111
+ while IFS= read -r f; do [ -n "$f" ] && layers+=("$f"); done < <(kit_resolve_layers "$start")
112
+ for f in ${layers[@]+"${layers[@]}"}; do
113
+ if jq -e "$jqpath // empty" "$f" >/dev/null 2>&1; then origin="$f"; defs="$defs$f"$'\n'; fi
114
+ done
115
+ printf 'value: %s\n' "${val:-(empty)}"
116
+ printf 'set by: %s\n' "$origin"
117
+ printf 'layers (far->near):\n'
118
+ for f in ${layers[@]+"${layers[@]}"}; do
119
+ if printf '%s' "$defs" | grep -qxF "$f"; then printf ' * %s (defines %s)\n' "$f" "$jqpath"; else printf ' %s\n' "$f"; fi
120
+ done
121
+ }
122
+
123
+ # CLI — run ONLY when executed directly (kit_is_main handles bash+zsh).
124
+ _kit_cr_dir="$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)"
125
+ # shellcheck source=/dev/null
126
+ . "$_kit_cr_dir/kit-cli.sh"
127
+ if kit_is_main; then
128
+ _dir="$PWD"; _mode=""; _arg=""
129
+ while [ $# -gt 0 ]; do
130
+ case "$1" in
131
+ --dir) _dir="$2"; shift 2;;
132
+ --explain) _mode=explain; _arg="$2"; shift 2;;
133
+ --get) _mode=get; _arg="$2"; shift 2;;
134
+ --layers) _mode=layers; shift;;
135
+ -h|--help) echo "usage: kit-config-resolve.sh [--dir DIR] [--explain JQPATH|--get JQPATH|--layers]"; exit 0;;
136
+ *) echo "unknown arg: $1" >&2; exit 2;;
137
+ esac
138
+ done
139
+ case "$_mode" in
140
+ explain) kit_resolve_explain "$_arg" "$_dir";;
141
+ get) kit_resolve_get "$_arg" "$_dir";;
142
+ layers) kit_resolve_layers "$_dir";;
143
+ *) kit_resolve "$_dir";;
144
+ esac
145
+ fi
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env bash
2
+ # Loads .claude/kit.config.json into KIT_* environment variables.
3
+ # Source this from project scripts/skills: source scripts/lib/kit-config.sh && load_kit_config
4
+ # Requires: jq.
5
+ #
6
+ # Per-folder overrides: any .claudekit/config.json in an ancestor directory is deep-merged over the
7
+ # project config, nearest-wins (like .editorconfig). Projects with no .claudekit/ behave unchanged.
8
+
9
+ load_kit_config() {
10
+ local cfg="${KIT_CONFIG:-.claude/kit.config.json}"
11
+ if [[ ! -f "$cfg" ]]; then
12
+ echo "✗ $cfg not found. Run /kit-init (or scripts/init.sh) first." >&2
13
+ return 1
14
+ fi
15
+ command -v jq >/dev/null 2>&1 || { echo "✗ jq is required." >&2; return 1; }
16
+
17
+ export KIT_VERSION="$(jq -r '.kitVersion // "0.0.0"' "$cfg")"
18
+
19
+ export KIT_PROJECT_NAME="$(jq -r '.project.name' "$cfg")"
20
+ export KIT_PROJECT_SLUG="$(jq -r '.project.slug' "$cfg")"
21
+ export KIT_OWNER_NAME="$(jq -r '.project.owner' "$cfg")"
22
+ export KIT_LANG="$(jq -r '.project.language' "$cfg")"
23
+ export KIT_PROFILE="$(jq -r '.profile' "$cfg")"
24
+ export KIT_SKILL_PREFIX="$(jq -r '.skillPrefix // ""' "$cfg")"
25
+
26
+ export KIT_REPO="$(jq -r '.github.repo' "$cfg")"
27
+ export KIT_OWNER="$(jq -r '.github.owner' "$cfg")"
28
+ export KIT_BASE_BRANCH="$(jq -r '.github.baseBranch // "main"' "$cfg")" # integration branch (default main; e.g. develop)
29
+ export KIT_PROJECTS_V2="$(jq -r '.github.projectsV2' "$cfg")"
30
+ export KIT_PROJECT_NUMBER="$(jq -r '.github.projectNumber' "$cfg")"
31
+ export KIT_PROJECT_TITLE="$(jq -r '.github.projectTitle' "$cfg")"
32
+
33
+ export KIT_PLANS_FORMAT="$(jq -r '.plans.format' "$cfg")"
34
+ export KIT_PLANS_DIR="$(jq -r '.plans.dir' "$cfg")"
35
+
36
+ export KIT_KNOWLEDGE_DIR="$(jq -r '.knowledge.dir // "knowledge"' "$cfg")"
37
+
38
+ export KIT_MEMORY="$(jq -r '.memory.enabled' "$cfg")"
39
+ export KIT_WING="$(jq -r '.memory.wing' "$cfg")"
40
+
41
+ export KIT_SPECKIT="$(jq -r '.specKit.enabled // false' "$cfg")"
42
+
43
+ export KIT_ANNOTATE_ENABLED="$(jq -r '.annotate.enabled // false' "$cfg")"
44
+ export KIT_ANNOTATE_BACKEND="$(jq -r '.annotate.backend // ""' "$cfg")"
45
+ export KIT_ANNOTATE_FRAMEWORK="$(jq -r '.annotate.framework // ""' "$cfg")"
46
+
47
+ # Local model layer (scripts/lib/kit-local.sh) — mlx_lm.server for NL chores
48
+ export KIT_LOCAL_ENABLED="$(jq -r '.local.enabled // false' "$cfg")"
49
+ export KIT_LOCAL_PORT="$(jq -r '.local.port // 8080' "$cfg")"
50
+ export KIT_LOCAL_MODEL="$(jq -r '.local.model // "mlx-community/Qwen3-8B-4bit"' "$cfg")"
51
+
52
+ # Convenience arrays (newline-delimited)
53
+ KIT_ROLES="$(jq -r '.roles[]? // empty' "$cfg")"
54
+ KIT_MILESTONES="$(jq -r '.milestones[]? // empty' "$cfg")"
55
+ export KIT_ROLES KIT_MILESTONES
56
+
57
+ # Apply per-folder .claudekit/ overrides (no-op when none exist).
58
+ _kit_apply_claudekit_overlays "$cfg"
59
+ }
60
+
61
+ # Deep-merge ancestor .claudekit/config.json files over the project config (nearest-wins) and
62
+ # re-export the fields that make sense to override at folder scope. Safe no-op when none are found.
63
+ _kit_apply_claudekit_overlays() {
64
+ local cfg="$1" proot d merged o
65
+ local overlays=()
66
+ proot="$(cd "$(dirname "$cfg")/.." 2>/dev/null && pwd)" || return 0
67
+
68
+ d="$proot"
69
+ while [[ -n "$d" && "$d" != "/" ]]; do
70
+ [[ -f "$d/.claudekit/config.json" ]] && overlays=("$d/.claudekit/config.json" "${overlays[@]}")
71
+ d="$(dirname "$d")"
72
+ done
73
+ [[ -f "/.claudekit/config.json" ]] && overlays=("/.claudekit/config.json" "${overlays[@]}")
74
+ [[ ${#overlays[@]} -eq 0 ]] && return 0
75
+
76
+ merged="$(cat "$cfg")"
77
+ for o in "${overlays[@]}"; do # far → near; jq '*' lets the right (nearer) operand win
78
+ merged="$(jq -s '.[0] * .[1]' <(printf '%s' "$merged") "$o" 2>/dev/null || printf '%s' "$merged")"
79
+ done
80
+
81
+ local g; g() { printf '%s' "$merged" | jq -r "$1" 2>/dev/null; }
82
+ export KIT_LANG="$(g '.project.language // env.KIT_LANG')"
83
+ export KIT_PLANS_FORMAT="$(g '.plans.format // env.KIT_PLANS_FORMAT')"
84
+ export KIT_ANNOTATE_ENABLED="$(g '.annotate.enabled // false')"
85
+ export KIT_ANNOTATE_BACKEND="$(g '.annotate.backend // ""')"
86
+ export KIT_ANNOTATE_FRAMEWORK="$(g '.annotate.framework // ""')"
87
+ export KIT_EFFECTIVE_CONFIG="$merged"
88
+ }