@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,107 @@
1
+ #!/usr/bin/env bash
2
+ # kit-engine-test.sh — self-test for the kit engine core (manifest + resolver + operate), #368.
3
+ # Libs are sourced into whatever shell the session runs (zsh on macOS), so they must behave
4
+ # identically under bash AND zsh. Run: bash scripts/lib/kit-engine-test.sh
5
+ # Re-runs itself under every available shell; set KIT_ENGINE_TEST_INNER to run assertions in-process.
6
+
7
+ set -u
8
+ dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
9
+
10
+ if [ -n "${KIT_ENGINE_TEST_INNER:-}" ]; then
11
+ . "$dir/kit-manifest.sh"
12
+ . "$dir/kit-config-resolve.sh"
13
+ . "$dir/kit-operate.sh"
14
+ fail=0
15
+ t() { # t <label> <got> <want>
16
+ if [ "$2" != "$3" ]; then echo "FAIL($KIT_ENGINE_TEST_INNER): $1 -> got '[$2]' want '[$3]'"; fail=1
17
+ else echo "ok($KIT_ENGINE_TEST_INNER): $1"; fi
18
+ }
19
+
20
+ work="$(mktemp -d)"; trap 'rm -rf "$work"' EXIT
21
+
22
+ # --- cascade fixture: workspace -> project -> island ---------------------
23
+ mkdir -p "$work/ws/proj/island/.claude" "$work/ws/proj/.claude" "$work/ws"
24
+ printf '{"project":{"language":"en"},"mode":"guided","roles":["pm","tech-lead"]}\n' > "$work/ws/kit.workspace.json"
25
+ printf '{"project":{"language":"es","name":"proj"},"mode":"enforced"}\n' > "$work/ws/proj/kit.config.json"
26
+ printf '{"mode":"enforced","roles":["backend"]}\n' > "$work/ws/proj/island/kit.config.json"
27
+
28
+ # layers far->near from island: workspace, project, island
29
+ layers="$(kit_resolve_layers "$work/ws/proj/island" | sed "s#$work/##g" | tr '\n' '|')"
30
+ t "layers order" "$layers" "ws/kit.workspace.json|ws/proj/kit.config.json|ws/proj/island/kit.config.json|"
31
+
32
+ # scalar override: language es (project) beats en (workspace)
33
+ t "scalar override (language)" "$(kit_resolve_get '.project.language' "$work/ws/proj/island")" "es"
34
+ # deep-merge keeps project name even though island doesn't mention project
35
+ t "deep-merge keeps name" "$(kit_resolve_get '.project.name' "$work/ws/proj/island")" "proj"
36
+ # array replace: island roles replace workspace roles (replace mode)
37
+ t "array replace (roles)" "$(kit_resolve_get '.roles | join(",")' "$work/ws/proj/island")" "backend"
38
+ # at project level (no island), roles come from workspace
39
+ t "array inherit at project" "$(kit_resolve_get '.roles | join(",")' "$work/ws/proj")" "pm,tech-lead"
40
+ # mode: enforced at island
41
+ t "mode at island" "$(kit_resolve_get '.mode' "$work/ws/proj/island")" "enforced"
42
+
43
+ # --explain origin: language is set by the project config (nearest definer)
44
+ origin="$(kit_resolve_explain '.project.language' "$work/ws/proj/island" | awk -F': ' '/^set by/{print $2}' | sed "s#$work/##")"
45
+ t "explain origin (language)" "$origin" "ws/proj/kit.config.json"
46
+
47
+ # concat mode: arrays concatenate
48
+ cm="$(KIT_MERGE_MODE=concat kit_resolve_get '.roles | join(",")' "$work/ws/proj/island")"
49
+ t "concat mode (roles)" "$cm" "pm,tech-lead,backend"
50
+
51
+ # --- manifest ------------------------------------------------------------
52
+ proj="$work/p"; mkdir -p "$proj/.claude"; cd "$proj"
53
+ export KIT_MANIFEST=".claude/kit.manifest.json"
54
+ printf 'hello\n' > tracked.txt
55
+ kit_manifest_record "tracked.txt" "A" "init" "1.0.0" "2026-01-01T00:00:00Z"
56
+ t "verify intact" "$(kit_manifest_verify tracked.txt)" "intact"
57
+ printf 'changed\n' > tracked.txt
58
+ t "verify modified" "$(kit_manifest_verify tracked.txt)" "modified"
59
+ rm -f tracked.txt
60
+ t "verify missing" "$(kit_manifest_verify tracked.txt)" "missing"
61
+ t "verify untracked" "$(kit_manifest_verify other.txt)" "untracked"
62
+ printf 'x\n' > b.txt; kit_manifest_record "b.txt" "B" "wire" "1.0.0" "2026-01-01T00:00:00Z"
63
+ t "list tier B" "$(kit_manifest_list B | tr '\n' ',')" "b.txt,"
64
+ kit_manifest_remove_entry "b.txt"
65
+ t "remove entry" "$(kit_manifest_verify b.txt)" "untracked"
66
+
67
+ # --- operate -------------------------------------------------------------
68
+ printf 'SRC v1\n' > src.txt
69
+ # dry-run writes nothing
70
+ KIT_DRY_RUN=1 kit_op_write src.txt dst.txt A wire >/dev/null 2>&1
71
+ t "dry-run no write" "$([ -f dst.txt ] && echo yes || echo no)" "no"
72
+ # real write with assume-yes
73
+ KIT_ASSUME_YES=1 kit_op_write src.txt dst.txt A wire >/dev/null 2>&1
74
+ t "op write created" "$([ -f dst.txt ] && echo yes || echo no)" "yes"
75
+ t "op write tracked" "$(kit_manifest_verify dst.txt)" "intact"
76
+ # idempotent: same content, no error, still intact
77
+ KIT_ASSUME_YES=1 kit_op_write src.txt dst.txt A wire >/dev/null 2>&1
78
+ t "op write idempotent" "$(kit_manifest_verify dst.txt)" "intact"
79
+ # conffiles: user edits dst, then op WITHOUT assume-yes must not clobber (declined => exit 10)
80
+ printf 'USER EDIT\n' > dst.txt
81
+ KIT_ASSUME_YES= kit_op_write src.txt dst.txt A wire </dev/null >/dev/null 2>&1
82
+ t "conffiles keeps edit" "$(cat dst.txt)" "USER EDIT"
83
+ # remove untracked refuses
84
+ printf 'z\n' > untracked2.txt
85
+ KIT_ASSUME_YES=1 kit_op_remove untracked2.txt >/dev/null 2>&1
86
+ t "remove refuses untracked" "$([ -f untracked2.txt ] && echo yes || echo no)" "yes"
87
+ # remove tracked-intact deletes (record fresh first to match current content)
88
+ printf 'SRC v1\n' > dst.txt; kit_manifest_record dst.txt A wire 1.0.0 2026-01-01T00:00:00Z
89
+ KIT_ASSUME_YES=1 kit_op_remove dst.txt >/dev/null 2>&1
90
+ t "remove intact deletes" "$([ -f dst.txt ] && echo yes || echo no)" "no"
91
+
92
+ [ "$fail" -eq 0 ] && echo "ALL OK($KIT_ENGINE_TEST_INNER)"
93
+ exit "$fail"
94
+ fi
95
+
96
+ rc=0; ran=0
97
+ for sh in bash zsh; do
98
+ command -v "$sh" >/dev/null 2>&1 || continue
99
+ ran=$((ran+1))
100
+ echo "--- $sh ---"
101
+ # -f / --no-rcs: skip user startup files (they can reset PATH); pass PATH explicitly so the
102
+ # subshell finds jq + coreutils regardless of the invoked shell's default environment.
103
+ rcflag=""; [ "$sh" = "zsh" ] && rcflag="--no-rcs"
104
+ KIT_ENGINE_TEST_INNER="$sh" PATH="$PATH" "$sh" $rcflag "$0" || rc=1
105
+ done
106
+ [ "$ran" -eq 0 ] && { echo "no shell found"; exit 1; }
107
+ exit "$rc"
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/lib/kit-events.sh — the kit event bus (append-only JSONL).
3
+ #
4
+ # A durable, single-writer-safe event stream that kit ops emit to and the kit-ui
5
+ # (packages/kit-ui) tails. Lives under $(git rev-parse --git-common-dir) so it is
6
+ # WORKTREE-DURABLE and survives ephemeral session mounts (same home as kit-sessions
7
+ # / kit-usage.jsonl). No second implementation: every kit op that wants to surface
8
+ # progress to the TUI sources this file and calls emit_event.
9
+ #
10
+ # Event shape (one JSON object per line):
11
+ # { "ts": "<ISO-8601 UTC>", "type": "<type>", "op": "<op-name>", "payload": {…} }
12
+ #
13
+ # Types (the contract — packages/kit-ui/CONTRACT.md is the source of truth):
14
+ # op.start | op.progress | op.done — lifecycle of a kit operation
15
+ # notice — a SessionStart-style notice (consolidated in the notice feed)
16
+ # collision — a worktree/file collision between two flows
17
+ # update-available — a kit version update is available
18
+ # flow.status — an orchestrate flow changed state
19
+ #
20
+ # Public API (source this file, then call):
21
+ # emit_event <type> <op> [payload-json] -> appends one event line; payload defaults to {}
22
+ # kit_events_path -> prints the absolute path to kit-events.jsonl
23
+ #
24
+ # Always non-fatal: a failure to emit (no jq, unwritable dir) never breaks the caller.
25
+
26
+ # Resolve the durable, absolute path to the event log.
27
+ # git-common-dir may be relative (".git") — resolve it against the repo root.
28
+ kit_events_path() {
29
+ local gcd
30
+ gcd="$(git rev-parse --git-common-dir 2>/dev/null)" || return 1
31
+ case "$gcd" in
32
+ /*) : ;; # already absolute
33
+ *) gcd="$(cd "$gcd" 2>/dev/null && pwd)" || return 1 ;;
34
+ esac
35
+ printf '%s/kit-events.jsonl\n' "$gcd"
36
+ }
37
+
38
+ # emit_event <type> <op> [payload-json]
39
+ # payload-json must be a valid JSON object string; defaults to {}.
40
+ emit_event() {
41
+ local type="${1:-}" op="${2:-}" payload="${3:-}"
42
+ [[ -n "$type" && -n "$op" ]] || return 0
43
+ [[ -n "$payload" ]] || payload='{}'
44
+ command -v jq >/dev/null 2>&1 || return 0
45
+
46
+ local path ts line
47
+ path="$(kit_events_path)" || return 0
48
+ ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
49
+
50
+ # Build the line with jq so payload is validated + the record is well-formed.
51
+ # If payload isn't valid JSON, fall back to wrapping it as a string note.
52
+ line="$(jq -cn \
53
+ --arg ts "$ts" --arg type "$type" --arg op "$op" \
54
+ --argjson payload "$payload" \
55
+ '{ts:$ts, type:$type, op:$op, payload:$payload}' 2>/dev/null)" || \
56
+ line="$(jq -cn \
57
+ --arg ts "$ts" --arg type "$type" --arg op "$op" --arg raw "$payload" \
58
+ '{ts:$ts, type:$type, op:$op, payload:{note:$raw}}' 2>/dev/null)" || return 0
59
+
60
+ # Single-writer append; >> is atomic for short lines on local fs.
61
+ printf '%s\n' "$line" >> "$path" 2>/dev/null || return 0
62
+ }
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env bash
2
+ # kit-gc.sh — the canonical "garbage-collect the repo" git-mechanic (#373 / #419 extraction).
3
+ #
4
+ # Plugin mirror of the canonical scripts/lib/kit-gc.sh (#370 self-contained). Same op, one home.
5
+ # Family 1 of kit-engine-boundary.md (rule #1/#2): ONE bash home for the gc op, consumed by the
6
+ # kit-gc skill, `kit gc`, and the kit-ui Run cockpit (#816 — it shells `scripts/kit gc`). No second
7
+ # implementation. This extracts the read-only ANALYSIS — branch/worktree/stash classification with
8
+ # the issue-open protection — out of the skill so a UI can run a REAL verb (not just preview text).
9
+ #
10
+ # The DESTRUCTIVE prune stays interactive (the skill / a human drives the confirmed deletes); a
11
+ # headless surface only ever runs the analysis. That split is deliberate: `kit_gc_analyze` is safe
12
+ # to run anywhere, anytime (it writes nothing), so the cockpit can flip its `gc` verb to runnable.
13
+ #
14
+ # kit_gc_analyze print the classification table (read-only). rc 0 always.
15
+ # kit_gc_has_prunable rc 0 if anything is safe to delete (for a UI badge / nudge).
16
+ #
17
+ # Requires: git; gh (degrades to "unknown" issue/PR state without it); scripts/lib/worktree-issue.sh.
18
+ # Portable: bash 3.2+ AND zsh.
19
+
20
+ KIT_GC_REPO="${KIT_GC_REPO:-${KIT_REPO:-}}"
21
+
22
+ _kit_gc_root() { git worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2; exit}'; }
23
+
24
+ # Source worktree-issue.sh (wt_issue_number / wt_protected_reason) from whatever lib dir we live in.
25
+ _kit_gc_load_deps() {
26
+ command -v wt_protected_reason >/dev/null 2>&1 && return 0
27
+ local d; d="$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)"
28
+ # shellcheck source=/dev/null
29
+ [ -f "$d/worktree-issue.sh" ] && . "$d/worktree-issue.sh"
30
+ }
31
+
32
+ # kit_gc_analyze — read-only classification of worktrees, branches, and stashes. Writes NOTHING.
33
+ # Each row is tagged PROTECTED / SAFE / ACTIVE / ORPHAN so a human or UI can decide what to prune.
34
+ kit_gc_analyze() {
35
+ _kit_gc_load_deps
36
+ local repo="$KIT_GC_REPO" b ref path reason pr prot
37
+ git fetch origin --prune --quiet 2>/dev/null || true
38
+
39
+ echo "# worktrees"
40
+ git worktree list --porcelain 2>/dev/null \
41
+ | awk '/^worktree /{w=$2} /^branch /{print w" "$2}' \
42
+ | while read -r path ref; do
43
+ b="${ref#refs/heads/}"
44
+ reason="$(wt_protected_reason "$b" "$repo" 2>/dev/null || true)"
45
+ if [ -n "$reason" ]; then echo " $path [$b] -> PROTECTED: $reason"
46
+ else echo " $path [$b] -> prunable if PR merged"; fi
47
+ done
48
+
49
+ echo "# branches"
50
+ for b in $(git branch --format='%(refname:short)' 2>/dev/null); do
51
+ case "$b" in develop|main) echo " $b -> ACTIVE (base branch)"; continue;; esac
52
+ pr="$(gh pr list --repo "$repo" --head "$b" --state all --json number,state --jq '.[0]|"PR#\(.number) \(.state)"' 2>/dev/null || true)"
53
+ prot="$(wt_protected_reason "$b" "$repo" 2>/dev/null || true)"
54
+ if [ -n "$prot" ]; then
55
+ echo " $b -> PROTECTED: $prot"
56
+ elif printf '%s' "$pr" | grep -q 'MERGED'; then
57
+ echo " $b -> SAFE (${pr}, issue closed/absent — verify level with remote before delete)"
58
+ elif printf '%s' "$pr" | grep -q 'OPEN'; then
59
+ echo " $b -> ACTIVE (${pr})"
60
+ else
61
+ echo " $b -> ${pr:-ORPHAN (no PR — surface, never auto-delete)}"
62
+ fi
63
+ done
64
+
65
+ echo "# stashes"
66
+ git stash list 2>/dev/null | sed 's/^/ /' || true
67
+ }
68
+
69
+ # kit_gc_has_prunable — rc 0 if at least one branch is SAFE to delete (a merged, unprotected branch).
70
+ kit_gc_has_prunable() {
71
+ kit_gc_analyze 2>/dev/null | grep -q '> SAFE '
72
+ }
73
+
74
+ # kit_gc_prune [--yes] - remove worktrees + local branches whose PR is MERGED (the SAFE rows).
75
+ # DRY-RUN by default (lists what it WOULD remove); --yes performs the deletions. Never touches a
76
+ # PROTECTED/ACTIVE/ORPHAN branch, and never a DIRTY worktree (recover-before-prune): a worktree with
77
+ # staged/unstaged/untracked changes is skipped with a warning, not destroyed. The remote branch is
78
+ # already deleted at merge time (gh pr merge --delete-branch); this cleans up the local side.
79
+ kit_gc_prune() {
80
+ _kit_gc_load_deps
81
+ local repo="$KIT_GC_REPO" yes=0 a path ref b pr
82
+ for a in "$@"; do case "$a" in --yes|-y) yes=1 ;; esac; done
83
+
84
+ # Worktrees first - a branch's worktree must be removed before the branch can be deleted.
85
+ git worktree list --porcelain 2>/dev/null \
86
+ | awk '/^worktree /{w=$2} /^branch /{print w" "$2}' \
87
+ | while read -r path ref; do
88
+ b="${ref#refs/heads/}"
89
+ case "$b" in develop|main|"") continue ;; esac
90
+ [ -n "$(wt_protected_reason "$b" "$repo" 2>/dev/null || true)" ] && continue
91
+ pr="$(gh pr list --repo "$repo" --head "$b" --state all --json state --jq '.[0].state' 2>/dev/null || true)"
92
+ [ "$pr" = "MERGED" ] || continue
93
+ if [ -n "$(git -C "$path" status --porcelain 2>/dev/null)" ]; then
94
+ echo " SKIP dirty worktree $path [$b] - commit/recover before pruning" >&2; continue
95
+ fi
96
+ if [ "$yes" -eq 1 ]; then
97
+ git worktree remove --force "$path" 2>/dev/null && echo " removed worktree $path [$b]"
98
+ else
99
+ echo " would remove worktree $path [$b] (PR MERGED)"
100
+ fi
101
+ done
102
+ git worktree prune 2>/dev/null || true
103
+
104
+ # Then local branches whose PR merged (worktree now gone).
105
+ for b in $(git branch --format='%(refname:short)' 2>/dev/null); do
106
+ case "$b" in develop|main) continue ;; esac
107
+ [ -n "$(wt_protected_reason "$b" "$repo" 2>/dev/null || true)" ] && continue
108
+ pr="$(gh pr list --repo "$repo" --head "$b" --state all --json state --jq '.[0].state' 2>/dev/null || true)"
109
+ [ "$pr" = "MERGED" ] || continue
110
+ if [ "$yes" -eq 1 ]; then
111
+ git branch -D "$b" >/dev/null 2>&1 && echo " deleted local branch $b (PR MERGED)"
112
+ else
113
+ echo " would delete local branch $b (PR MERGED)"
114
+ fi
115
+ done
116
+ [ "$yes" -eq 1 ] && echo "gc prune: done" || echo "gc prune: DRY RUN - pass --yes to delete"
117
+ }
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bash
2
+ # kit-interview-test.sh — self-test for kit-interview (#372). Runs under bash AND zsh.
3
+ # Run: bash scripts/lib/kit-interview-test.sh
4
+ dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
5
+
6
+ if [ -n "${KIT_IV_TEST_INNER:-}" ]; then
7
+ set -u
8
+ . "$dir/kit-interview.sh"
9
+ fail=0
10
+ t() { if [ "$2" != "$3" ]; then echo "FAIL($KIT_IV_TEST_INNER): $1 -> got '[$2]' want '[$3]'"; fail=1; else echo "ok($KIT_IV_TEST_INNER): $1"; fi; }
11
+
12
+ export KIT_PROFILE_HOME="$(mktemp -d)"
13
+ work="$(mktemp -d)"; trap 'rm -rf "$KIT_PROFILE_HOME" "$work"' EXIT
14
+
15
+ # --- catalogs load + tier matches + module routing ---
16
+ t "catalog global tier" "$(kit_interview_catalog global | jq -r .tier)" "global"
17
+ t "catalog project tier" "$(kit_interview_catalog project | jq -r .tier)" "project"
18
+ t "catalog software tier" "$(kit_interview_catalog software | jq -r .tier)" "software"
19
+ case "$(kit_interview_catalog_file software)" in
20
+ */modules/software.json) t "software routes to modules/" yes yes;;
21
+ *) t "software routes to modules/" "$(kit_interview_catalog_file software)" "*/modules/software.json";;
22
+ esac
23
+ kit_interview_catalog nope >/dev/null 2>&1; t "unknown tier rc" "$?" "1"
24
+
25
+ # --- context: repo detection ---
26
+ mkdir -p "$work/proj"; printf '{}' > "$work/proj/package.json"
27
+ ctx="$(kit_interview_context "$work/proj")"
28
+ t "ctx repo.dir" "$(printf '%s' "$ctx" | jq -r .repo.dir)" "proj"
29
+ t "ctx repo.language" "$(printf '%s' "$ctx" | jq -r .repo.language)" "javascript"
30
+ t "ctx repo.hasGit" "$(printf '%s' "$ctx" | jq -r .repo.hasGit)" "false"
31
+
32
+ # --- render: per-project pre-fill from profile + repo ---
33
+ . "$dir/kit-profile.sh"
34
+ kit_profile_set name "Ada" string tester
35
+ kit_profile_set language "es" string tester
36
+ rp="$(KIT_PROFILE_USER=tester kit_interview_render project "$work/proj")"
37
+ t "render name<-repo.dir" "$(printf '%s' "$rp" | jq -r '.questions[]|select(.key=="name").default')" "proj"
38
+ t "render owner<-profile" "$(printf '%s' "$rp" | jq -r '.questions[]|select(.key=="owner").default')" "Ada"
39
+ t "render lang<-profile" "$(printf '%s' "$rp" | jq -r '.questions[]|select(.key=="language").default')" "es"
40
+
41
+ # --- apply project: text + select targets ---
42
+ printf '%s\n' '{"name":"My App","owner":"Ada","language":"es","mode":"enforced","software":"no"}' > "$work/ans-proj.json"
43
+ ap="$(kit_interview_apply project "$work/ans-proj.json")"
44
+ t "apply project.name" "$(printf '%s' "$ap" | jq -r .project.name)" "My App"
45
+ t "apply mode" "$(printf '%s' "$ap" | jq -r .mode)" "enforced"
46
+ t "control no key" "$(printf '%s' "$ap" | jq -r 'has("software")')" "false"
47
+
48
+ # --- apply software wizard: sets + idempotent modules union ---
49
+ printf '%s\n' '{"versioning":"github","deploy":"vercel","ci":"github-actions"}' > "$work/ans-sw.json"
50
+ printf '%s\n' "$ap" > "$work/base.json"
51
+ sw1="$(kit_interview_apply software "$work/ans-sw.json" "$work/base.json")"
52
+ t "sw modules" "$(printf '%s' "$sw1" | jq -c .modules)" '["software"]'
53
+ t "sw github" "$(printf '%s' "$sw1" | jq -r .github.projectsV2)" "true"
54
+ t "sw deploy" "$(printf '%s' "$sw1" | jq -r .deploy.target)" "vercel"
55
+ t "sw ci" "$(printf '%s' "$sw1" | jq -r .ci.provider)" "github-actions"
56
+ printf '%s\n' "$sw1" > "$work/base2.json"
57
+ sw2="$(kit_interview_apply software "$work/ans-sw.json" "$work/base2.json")"
58
+ t "sw modules idempotent" "$(printf '%s' "$sw2" | jq -c .modules)" '["software"]'
59
+
60
+ # --- apply with missing answer falls back to default ---
61
+ printf '%s\n' '{}' > "$work/ans-empty.json"
62
+ swd="$(kit_interview_apply software "$work/ans-empty.json")"
63
+ t "apply default deploy" "$(printf '%s' "$swd" | jq -r .deploy.target)" "none"
64
+
65
+ [ "$fail" -eq 0 ] && echo "ALL OK($KIT_IV_TEST_INNER)"
66
+ exit "$fail"
67
+ fi
68
+
69
+ rc=0; ran=0
70
+ for sh in bash zsh; do
71
+ command -v "$sh" >/dev/null 2>&1 || continue
72
+ ran=$((ran+1)); echo "--- $sh ---"
73
+ rcflag=""; [ "$sh" = "zsh" ] && rcflag="--no-rcs"
74
+ KIT_IV_TEST_INNER="$sh" PATH="$PATH" "$sh" $rcflag "$0" || rc=1
75
+ done
76
+ [ "$ran" -eq 0 ] && { echo "no shell"; exit 1; }
77
+ exit "$rc"
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env bash
2
+ # kit-interview.sh — the deterministic brain of the two-tier onboarding interview (D11, #372).
3
+ #
4
+ # THE SPLIT: the ASKING is interactive (the kit-onboard skill drives AskUserQuestion — Tier A,
5
+ # portable to Cowork/claude.ai). The DERIVING is pure data and lives here, so it is unit-testable
6
+ # under bash AND zsh with zero interactivity:
7
+ # - catalog : the question set per tier (global | project | software), loaded from interview/*.json
8
+ # and modules/*.json — data, reviewable, not buried in bash.
9
+ # - context : profile + repo detection, used to PRE-FILL per-project / wizard defaults.
10
+ # - render : the catalog with every `default` resolved from context (what the skill renders).
11
+ # - apply : answers {key:value} + a base config -> merged config JSON (no side effects).
12
+ # The caller (kit-onboard skill / kit-add.sh) persists the merged JSON — to the global profile
13
+ # directly, or to a project's .claude/kit.config.json through kit-operate (so it is manifest-tracked).
14
+ #
15
+ # Source it: source scripts/lib/kit-interview.sh
16
+ # CLI: kit-interview.sh --catalog TIER
17
+ # kit-interview.sh --context [--dir DIR]
18
+ # kit-interview.sh --render TIER [--dir DIR]
19
+ # kit-interview.sh --apply TIER --answers FILE [--base FILE]
20
+ # Requires: jq.
21
+
22
+ _kit_iv_dir="$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)"
23
+ # shellcheck source=/dev/null
24
+ . "$_kit_iv_dir/kit-cli.sh" # kit_is_main, kit_say/warn/die
25
+ # shellcheck source=/dev/null
26
+ . "$_kit_iv_dir/kit-profile.sh" # kit_profile_read
27
+
28
+ # Plugin root = two levels up from scripts/lib/. Override with KIT_PLUGIN_ROOT (tests).
29
+ kit_iv_plugin_root() {
30
+ if [ -n "${KIT_PLUGIN_ROOT:-}" ]; then printf '%s\n' "$KIT_PLUGIN_ROOT"; return 0; fi
31
+ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then printf '%s\n' "$CLAUDE_PLUGIN_ROOT"; return 0; fi
32
+ ( CDPATH='' cd -- "$_kit_iv_dir/../.." && pwd )
33
+ }
34
+
35
+ _kit_iv_require() { command -v jq >/dev/null 2>&1 || { kit_warn "kit-interview: jq is required"; return 1; }; }
36
+
37
+ # kit_interview_catalog_file <tier> -> path of the catalog JSON for a tier.
38
+ # global|project -> interview/<tier>.json ; anything else -> modules/<tier>.json (software, ...)
39
+ kit_interview_catalog_file() {
40
+ local tier="$1" root; root="$(kit_iv_plugin_root)"
41
+ case "$tier" in
42
+ global|project) printf '%s/interview/%s.json\n' "$root" "$tier";;
43
+ *) printf '%s/modules/%s.json\n' "$root" "$tier";;
44
+ esac
45
+ }
46
+
47
+ # kit_interview_catalog <tier> -> the catalog JSON (verbatim)
48
+ kit_interview_catalog() {
49
+ _kit_iv_require || return 1
50
+ local f; f="$(kit_interview_catalog_file "$1")"
51
+ [ -f "$f" ] || { kit_warn "kit-interview: no catalog for tier '$1' ($f)"; return 1; }
52
+ jq -e . "$f" >/dev/null 2>&1 || { kit_warn "kit-interview: invalid JSON in $f"; return 1; }
53
+ cat "$f"
54
+ }
55
+
56
+ # --- repo detection (per-project pre-fill) --------------------------------
57
+ # kit_interview_context [dir] -> { profile:{...}, repo:{dir,language,hasGit,remote,hasGitHub} }
58
+ kit_interview_context() {
59
+ _kit_iv_require || return 1
60
+ local dir; dir="$(cd "${1:-$PWD}" 2>/dev/null && pwd)" || dir="${1:-$PWD}"
61
+ local base; base="$(basename "$dir")"
62
+ local lang="" hasgit=false remote="" hasgh=false
63
+ [ -f "$dir/package.json" ] && lang="javascript"
64
+ [ -f "$dir/pyproject.toml" ] && lang="python"
65
+ [ -f "$dir/Cargo.toml" ] && lang="rust"
66
+ [ -f "$dir/go.mod" ] && lang="go"
67
+ if git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
68
+ hasgit=true
69
+ local url; url="$(git -C "$dir" remote get-url origin 2>/dev/null || true)"
70
+ if [ -n "$url" ]; then
71
+ hasgh=true
72
+ remote="$(printf '%s' "$url" | sed -E 's#^git@github.com:##; s#^https://github.com/##; s#\.git$##')"
73
+ fi
74
+ fi
75
+ local profile; profile="$(kit_profile_read)"
76
+ jq -n --argjson p "$profile" --arg d "$base" --arg l "$lang" \
77
+ --argjson g "$hasgit" --arg r "$remote" --argjson gh "$hasgh" \
78
+ '{profile:$p, repo:{dir:$d, language:$l, hasGit:$g, remote:$r, hasGitHub:$gh}}'
79
+ }
80
+
81
+ # kit_interview_render <tier> [dir] -> the catalog with each question.default resolved from context.
82
+ # For a question with "prefillFrom":"profile.language", if context.<prefillFrom> is non-empty it
83
+ # becomes the default. This is what the skill renders into AskUserQuestion.
84
+ kit_interview_render() {
85
+ _kit_iv_require || return 1
86
+ local tier="$1" dir="${2:-$PWD}" cat ctx
87
+ cat="$(kit_interview_catalog "$tier")" || return 1
88
+ ctx="$(kit_interview_context "$dir")" || return 1
89
+ jq --argjson ctx "$ctx" '
90
+ def deref($path): ($path | split(".")) as $p | reduce $p[] as $k ($ctx; if type=="object" then .[$k] else null end);
91
+ .questions |= map(
92
+ if (.prefillFrom // "") != ""
93
+ then (deref(.prefillFrom)) as $v | if ($v != null and $v != "") then .default = ($v|tostring) else . end
94
+ else . end
95
+ )
96
+ ' <<EOF
97
+ $cat
98
+ EOF
99
+ }
100
+
101
+ # --- apply: answers + base config -> merged config (pure) -----------------
102
+ # _kit_iv_setpath <dotpath> <value> <valtype> : jq filter snippet applied to stdin JSON.
103
+ # valtype: string | bool | number | json. For valtype json on an existing array, unions uniquely
104
+ # (so adding a module twice is a no-op — idempotent modules:[]).
105
+ _kit_iv_apply_set() {
106
+ local dotpath="$1" value="$2" valtype="${3:-string}" # `path` is $PATH-tied in zsh — never use it
107
+ local setexpr
108
+ setexpr="$(printf '%s' "$dotpath" | awk -F. '{out="["; for(i=1;i<=NF;i++){ if(i>1)out=out","; out=out"\""$i"\""}; out=out"]"; print out}')"
109
+ case "$valtype" in
110
+ bool|boolean|number|int)
111
+ jq --argjson v "$value" "setpath($setexpr; \$v)";;
112
+ json)
113
+ jq --argjson v "$value" "
114
+ getpath($setexpr) as \$cur
115
+ | if (\$cur|type)==\"array\" and (\$v|type)==\"array\"
116
+ then setpath($setexpr; (\$cur + \$v | unique))
117
+ else setpath($setexpr; \$v) end";;
118
+ *)
119
+ jq --arg v "$value" "setpath($setexpr; \$v)";;
120
+ esac
121
+ }
122
+
123
+ # kit_interview_apply <tier> <answersfile> [basefile] -> merged config JSON on stdout.
124
+ # answersfile: {"<key>":"<chosen value>", ...}. Unanswered questions fall back to their default.
125
+ # Control questions (e.g. project.software) carry no `set` — they steer the caller, not the config.
126
+ kit_interview_apply() {
127
+ _kit_iv_require || return 1
128
+ # ALL locals declared ONCE up front. A BARE `local x` re-run inside a loop PRINTS the var under
129
+ # zsh (treated like `typeset x`), polluting stdout — so never re-declare locals in the loops below.
130
+ local tier="$1" ans="$2" base="${3:-}"
131
+ local catalog merged nsets nq i p v vt tmp q key qtype def answer target nset j
132
+ [ -f "$ans" ] || { kit_warn "kit-interview apply: answers file missing: $ans"; return 1; }
133
+ catalog="$(kit_interview_catalog "$tier")" || return 1
134
+ merged="$(mktemp)" || return 1
135
+ if [ -n "$base" ] && [ -f "$base" ]; then cp "$base" "$merged"; else printf '{}\n' > "$merged"; fi
136
+
137
+ # 1. unconditional catalog-level sets (e.g. modules:["software"])
138
+ nsets="$(printf '%s' "$catalog" | jq '(.sets // []) | length')"
139
+ i=0
140
+ while [ "$i" -lt "$nsets" ]; do
141
+ p="$(printf '%s' "$catalog" | jq -r ".sets[$i].path")"
142
+ v="$(printf '%s' "$catalog" | jq -r ".sets[$i].value")"
143
+ vt="$(printf '%s' "$catalog" | jq -r ".sets[$i].valtype // \"string\"")"
144
+ tmp="$(mktemp)"; _kit_iv_apply_set "$p" "$v" "$vt" < "$merged" > "$tmp" && mv "$tmp" "$merged" || { rm -f "$tmp" "$merged"; return 1; }
145
+ i=$((i+1))
146
+ done
147
+
148
+ # 2. per-question answers
149
+ nq="$(printf '%s' "$catalog" | jq '.questions | length')"
150
+ i=0
151
+ while [ "$i" -lt "$nq" ]; do
152
+ q="$(printf '%s' "$catalog" | jq ".questions[$i]")"
153
+ key="$(printf '%s' "$q" | jq -r '.key')"
154
+ qtype="$(printf '%s' "$q" | jq -r '.type // "text"')"
155
+ def="$(printf '%s' "$q" | jq -r '.default // ""')"
156
+ answer="$(jq -r --arg k "$key" '.[$k] // empty' "$ans")"
157
+ [ -z "$answer" ] && answer="$def"
158
+
159
+ if [ "$qtype" = "text" ]; then
160
+ target="$(printf '%s' "$q" | jq -r '.target // empty')"
161
+ if [ -n "$target" ] && [ -n "$answer" ]; then
162
+ tmp="$(mktemp)"; _kit_iv_apply_set "$target" "$answer" string < "$merged" > "$tmp" && mv "$tmp" "$merged" || { rm -f "$tmp" "$merged"; return 1; }
163
+ fi
164
+ else
165
+ # select: apply the chosen option's set[] (control-only options have none)
166
+ nset="$(printf '%s' "$q" | jq --arg a "$answer" '[.options[] | select(.value==$a)] | .[0].set // [] | length')"
167
+ j=0
168
+ while [ "$j" -lt "$nset" ]; do
169
+ p="$(printf '%s' "$q" | jq -r --arg a "$answer" "[.options[] | select(.value==\$a)][0].set[$j].path")"
170
+ v="$(printf '%s' "$q" | jq -r --arg a "$answer" "[.options[] | select(.value==\$a)][0].set[$j].value")"
171
+ vt="$(printf '%s' "$q" | jq -r --arg a "$answer" "[.options[] | select(.value==\$a)][0].set[$j].valtype // \"string\"")"
172
+ tmp="$(mktemp)"; _kit_iv_apply_set "$p" "$v" "$vt" < "$merged" > "$tmp" && mv "$tmp" "$merged" || { rm -f "$tmp" "$merged"; return 1; }
173
+ j=$((j+1))
174
+ done
175
+ fi
176
+ i=$((i+1))
177
+ done
178
+
179
+ cat "$merged"; rm -f "$merged"
180
+ }
181
+
182
+ # CLI — direct execution only.
183
+ if kit_is_main; then
184
+ _mode=""; _tier=""; _dir="$PWD"; _ans=""; _base=""
185
+ while [ $# -gt 0 ]; do case "$1" in
186
+ --catalog) _mode=catalog; _tier="$2"; shift 2;;
187
+ --context) _mode=context; shift;;
188
+ --render) _mode=render; _tier="$2"; shift 2;;
189
+ --apply) _mode=apply; _tier="$2"; shift 2;;
190
+ --dir) _dir="$2"; shift 2;;
191
+ --answers) _ans="$2"; shift 2;;
192
+ --base) _base="$2"; shift 2;;
193
+ -h|--help) echo "usage: kit-interview.sh --catalog|--render TIER [--dir D] | --context [--dir D] | --apply TIER --answers F [--base F]"; exit 0;;
194
+ *) kit_warn "unknown arg: $1"; exit 2;;
195
+ esac; done
196
+ case "$_mode" in
197
+ catalog) kit_interview_catalog "$_tier";;
198
+ context) kit_interview_context "$_dir";;
199
+ render) kit_interview_render "$_tier" "$_dir";;
200
+ apply) kit_interview_apply "$_tier" "$_ans" "$_base";;
201
+ *) kit_warn "nothing to do (see --help)"; exit 2;;
202
+ esac
203
+ fi
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env bash
2
+ # kit-local.sh — local model client for claude-kit NL chores (digest, summarize, classify, draft).
3
+ # Backed by mlx_lm.server (Apple MLX), an OpenAI-compatible HTTP server on localhost.
4
+ #
5
+ # Setup (one-time — /kit-doctor does both automatically, #313):
6
+ # uv tool install mlx-lm # isolated venv, PEP 668-safe (fallback: pipx install mlx-lm)
7
+ # mlx_lm.server --model mlx-community/Qwen3-8B-4bit --port 8080
8
+ #
9
+ # Source this from hooks/skills: source scripts/lib/kit-local.sh
10
+ # kit_local_alive -> 0 if the server responds (fast: 1s timeout)
11
+ # kit_local_chat "<system>" "<prompt>" -> prints the model reply; non-zero on any failure
12
+ # kit_local_dismissed -> 0 if the "layer down" notice was dismissed
13
+ #
14
+ # HARD RULE — fallback always: every caller must treat a non-zero exit as "use the current
15
+ # (non-local) path". This lib never blocks a hook: alive-check 1s, chat bounded by
16
+ # KIT_LOCAL_TIMEOUT (default 90s; hooks should pass lower via env when latency matters).
17
+ # Config: .claude/kit.config.json -> .local {enabled, port, model} (KIT_LOCAL_* env wins).
18
+
19
+ KIT_LOCAL_CONFIG="${KIT_CONFIG:-.claude/kit.config.json}"
20
+
21
+ _kit_local_cfg() { # _kit_local_cfg <jq-path> <default>
22
+ local v=""
23
+ if command -v jq >/dev/null 2>&1 && [[ -f "$KIT_LOCAL_CONFIG" ]]; then
24
+ v="$(jq -r "$1 // empty" "$KIT_LOCAL_CONFIG" 2>/dev/null)"
25
+ fi
26
+ printf '%s' "${v:-$2}"
27
+ }
28
+
29
+ KIT_LOCAL_ENABLED="${KIT_LOCAL_ENABLED:-$(_kit_local_cfg '.local.enabled' 'false')}"
30
+ KIT_LOCAL_PORT="${KIT_LOCAL_PORT:-$(_kit_local_cfg '.local.port' '8080')}"
31
+ KIT_LOCAL_MODEL="${KIT_LOCAL_MODEL:-$(_kit_local_cfg '.local.model' 'mlx-community/Qwen3-8B-4bit')}"
32
+ KIT_LOCAL_URL="http://127.0.0.1:${KIT_LOCAL_PORT}"
33
+ KIT_LOCAL_TIMEOUT="${KIT_LOCAL_TIMEOUT:-90}"
34
+
35
+ # 0 if enabled in config AND the server answers /v1/models within 1s.
36
+ kit_local_alive() {
37
+ [[ "$KIT_LOCAL_ENABLED" == "true" ]] || return 1
38
+ command -v curl >/dev/null 2>&1 || return 1
39
+ curl -sf -m 1 "$KIT_LOCAL_URL/v1/models" >/dev/null 2>&1
40
+ }
41
+
42
+ # kit_local_chat "<system>" "<user prompt>" [max_tokens]
43
+ # Prints the assistant reply (reasoning <think> blocks stripped). Non-zero on any failure.
44
+ kit_local_chat() {
45
+ local system="$1" prompt="$2" max_tokens="${3:-1024}"
46
+ kit_local_alive || return 1
47
+ command -v jq >/dev/null 2>&1 || return 1
48
+
49
+ local payload reply
50
+ payload="$(jq -n --arg m "$KIT_LOCAL_MODEL" --arg s "$system" --arg p "$prompt" --argjson t "$max_tokens" \
51
+ '{model:$m, messages:[{role:"system",content:$s},{role:"user",content:$p}], max_tokens:$t, temperature:0.2}')" || return 1
52
+
53
+ reply="$(curl -sf -m "$KIT_LOCAL_TIMEOUT" "$KIT_LOCAL_URL/v1/chat/completions" \
54
+ -H 'Content-Type: application/json' \
55
+ -d "$payload" 2>/dev/null | jq -r '.choices[0].message.content // empty')" || return 1
56
+ [[ -n "$reply" ]] || return 1
57
+
58
+ # Qwen3 reasoning models may prefix a <think>...</think> block — strip it.
59
+ printf '%s' "$reply" | perl -0pe 's/<think>.*?<\/think>\s*//gs' 2>/dev/null || printf '%s' "$reply"
60
+ }
61
+
62
+ # Short model tag for banners: "Qwen3-8B-4bit" from "mlx-community/Qwen3-8B-4bit".
63
+ kit_local_model_tag() {
64
+ printf '%s' "${KIT_LOCAL_MODEL##*/}"
65
+ }
66
+
67
+ # 0 if the "local layer down" session notice is dismissed (#313). Dismiss channels:
68
+ # - env KIT_LOCAL_DISMISS=1 (this session/shell only)
69
+ # - config .local.dismissed = "<kitVersion at dismissal>" (written by kit-doctor
70
+ # --dismiss-local) — sticks until the kit's x.y core moves past it; "true" = forever
71
+ kit_local_dismissed() {
72
+ [[ "${KIT_LOCAL_DISMISS:-}" == "1" ]] && return 0
73
+ local d cur
74
+ d="$(_kit_local_cfg '.local.dismissed' '')"
75
+ [[ -n "$d" ]] || return 1
76
+ [[ "$d" == "true" ]] && return 0
77
+ cur="$(_kit_local_cfg '.kitVersion' '0.0.0')"
78
+ [[ "$(printf '%s' "$d" | cut -d. -f1-2)" == "$(printf '%s' "$cur" | cut -d. -f1-2)" ]]
79
+ }