@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,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
|
+
}
|