@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,83 @@
1
+ #!/usr/bin/env bash
2
+ # shellcheck shell=bash
3
+ # effort-ops-test.sh — covers the effort lifecycle ops (#48). Hermetic: stubs gh (no network/auth)
4
+ # and uses a throwaway git repo with a bare remote. Run: bash scripts/lib/effort-ops-test.sh
5
+ set -u
6
+ ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
7
+ LIB="$ROOT/scripts/lib"
8
+ fail=0
9
+ t() { if [ "$2" = "$3" ]; then echo "ok: $1"; else echo "FAIL: $1 -> got '[$2]' want '[$3]'"; fail=1; fi; }
10
+ tc() { if grep -qE "$2" "$1"; then echo "ok: $3"; else echo "FAIL: $3 (no /$2/ in gh log)"; fail=1; fi; }
11
+ command -v jq >/dev/null 2>&1 || { echo "effort-ops-test: jq required" >&2; exit 1; }
12
+ command -v git >/dev/null 2>&1 || { echo "effort-ops-test: git required" >&2; exit 1; }
13
+
14
+ tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
15
+ export GH_LOG="$tmp/gh.log"; export GH_N="$tmp/n"; : > "$GH_LOG"
16
+
17
+ # Stub gh: log every call, return canned output keyed on the subcommand.
18
+ stub="$tmp/bin"; mkdir -p "$stub"
19
+ cat > "$stub/gh" <<'SH'
20
+ #!/usr/bin/env bash
21
+ echo "$*" >> "$GH_LOG"
22
+ case "$1 $2" in
23
+ "issue create") n=$(( $(cat "$GH_N" 2>/dev/null || echo 0) + 1 )); echo "$n" > "$GH_N"
24
+ echo "https://github.com/o/r/issues/$n" ;;
25
+ "issue edit"|"issue close"|"pr merge") exit 0 ;;
26
+ "issue view") echo "[Effort] 99 · demo effort" ;; # --json title -q .title
27
+ "pr create") echo "https://github.com/o/r/pull/7" ;;
28
+ "api "*|"api")
29
+ case "$*" in
30
+ *"--method POST"*"/sub_issues"*) exit 0 ;; # link a sub
31
+ *"/sub_issues"*".[].number"*) printf '101\n102\n' ;; # list subs (for close)
32
+ *".id"*) echo "55501" ;; # issue db id
33
+ *) exit 0 ;;
34
+ esac ;;
35
+ *) exit 0 ;;
36
+ esac
37
+ SH
38
+ chmod +x "$stub/gh"
39
+ export PATH="$stub:$PATH"
40
+ export KIT_REPO="o/r" EFFORT_REPO="o/r" KIT_BASE_BRANCH="main"
41
+ # shellcheck source=/dev/null
42
+ source "$LIB/effort.sh" 2>/dev/null
43
+ # shellcheck source=/dev/null
44
+ source "$LIB/effort-ops.sh"
45
+
46
+ # ── effort_new ────────────────────────────────────────────────────────────────────────────────
47
+ : > "$GH_LOG"
48
+ parent="$(effort_new "[Core] demo effort" "first sub" "second sub" 2>/dev/null)"
49
+ t "effort_new returns the parent number" "$parent" "1"
50
+ t "effort_new creates parent + 2 subs (3 issues)" "$(grep -c 'issue create' "$GH_LOG")" "3"
51
+ t "effort_new links 2 native sub-issues" "$(grep -c 'method POST .*sub_issues' "$GH_LOG")" "2"
52
+ tc "$GH_LOG" 'issue create .*--title \[Effort\] · \[Core\] demo effort' "effort_new titles the parent"
53
+ # a jargon/long name is rejected before any issue is created
54
+ : > "$GH_LOG"
55
+ effort_new "refactor the whole scripts/kit wiring layer" >/dev/null 2>&1 && rc=0 || rc=1
56
+ t "effort_new rejects a bad title" "$rc" "1"
57
+ t "effort_new creates nothing on a bad title" "$(grep -c 'issue create' "$GH_LOG")" "0"
58
+
59
+ # ── effort_start / effort_pr / effort_close (real git + bare remote) ───────────────────────────
60
+ ( cd "$tmp" && git init -q --bare remote.git )
61
+ ( cd "$tmp" && git clone -q remote.git work \
62
+ && cd work && git -c user.email=t@t -c user.name=t commit -q --allow-empty -m init \
63
+ && git push -q origin HEAD:main )
64
+ cd "$tmp/work"
65
+
66
+ start_out="$(effort_start 99 demo 2>/dev/null)"
67
+ t "effort_start echoes wt|branch|num" "${start_out##*|}" "99"
68
+ t "effort_start created the branch" "$(git show-ref --verify --quiet refs/heads/effort/99-demo && echo yes)" "yes"
69
+
70
+ # move onto the effort branch (its worktree) for pr/close
71
+ cd "$tmp/work/.claude/worktrees/effort-99"
72
+ : > "$GH_LOG"
73
+ effort_pr 99 >/dev/null 2>&1
74
+ tc "$GH_LOG" 'pr create .*--base main --head effort/99-demo' "effort_pr opens effort/99 → main"
75
+
76
+ : > "$GH_LOG"
77
+ effort_close 99 >/dev/null 2>&1
78
+ tc "$GH_LOG" 'pr merge effort/99-demo .*--squash' "effort_close squash-merges the PR"
79
+ tc "$GH_LOG" 'issue close 101' "effort_close closes sub #101"
80
+ tc "$GH_LOG" 'issue close 99 ' "effort_close closes the parent"
81
+
82
+ [ "$fail" -eq 0 ] && echo "ALL OK (effort-ops)" || echo "effort-ops: FAILURES"
83
+ exit "$fail"
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env bash
2
+ # shellcheck shell=bash
3
+ # effort-ops.sh — the effort lifecycle as shell ops, so `cckit effort new|start|pr|close` works from
4
+ # any shell or agent (not only via the effort-* skills). Thin: composes the git-mechanics helpers in
5
+ # effort.sh (linking, snapshots, title lint) plus gh + git. bash 3.2 compatible. Requires: gh, jq, git.
6
+ #
7
+ # effort_new "<name>" [<sub title> …] parent issue (template) + native sub-issues, all linked
8
+ # effort_start <N> [<slug>] effort/<N> branch + worktree from the base branch
9
+ # effort_pr [<N>] open the ONE PR effort/<N> → base branch
10
+ # effort_close <N> snapshot sub-diffs, squash-merge the PR, close parent + subs
11
+ #
12
+ # Repo + base branch come from kit.config.json (EFFORT_REPO / KIT_BASE_BRANCH), loaded by effort.sh.
13
+
14
+ _eff_repo() { printf '%s' "${EFFORT_REPO:-${KIT_REPO:-}}"; }
15
+ _eff_base() { printf '%s' "${KIT_BASE_BRANCH:-main}"; }
16
+ _eff_slug() { # <text> → a short branch-safe slug (mirrors wt_start)
17
+ printf '%s' "$1" | sed -E 's/^\[[^]]+\][[:space:]]*//' | tr '[:upper:]' '[:lower:]' \
18
+ | sed -E 's/[^a-z0-9]+/-/g; s/^-+|-+$//g' | cut -c1-40
19
+ }
20
+ _eff_need() { command -v "$1" >/dev/null 2>&1 || { echo "effort: $1 is required" >&2; return 1; }; }
21
+
22
+ # The parent-issue body template (rules/effort-model.md): the four sections double as the work record.
23
+ _eff_parent_body() {
24
+ cat <<'EOF'
25
+ ## Goal
26
+ <!-- problem statement: what outcome, in one or two lines -->
27
+
28
+ ## Scope
29
+ <!-- the sub-issue plan; mark each parallel | sequential / dependsOn -->
30
+
31
+ ## For agents
32
+ <!-- exact file paths / entry points a future agent needs -->
33
+
34
+ ## Verification
35
+ <!-- how we know it's done: commands, checks, acceptance -->
36
+ EOF
37
+ }
38
+
39
+ # effort_new "<name>" [<sub title> …] — create the parent (template) + native sub-issues, linked.
40
+ effort_new() {
41
+ _eff_need gh || return 1; _eff_need jq || return 1
42
+ local repo name; repo="$(_eff_repo)"; name="${1:-}"; shift || true
43
+ [ -n "$repo" ] || { echo "effort_new: no repo (KIT_REPO/EFFORT_REPO unset — run in a kit project)" >&2; return 1; }
44
+ [ -n "$name" ] || { echo 'effort_new: usage: effort_new "<name>" [<sub title> …]' >&2; return 1; }
45
+ # Validate the name against the title rule BEFORE creating anything (synthetic prefix for the lint).
46
+ effort_title_lint "[Effort] 0 · $name" || { echo "effort_new: fix the name and retry" >&2; return 1; }
47
+
48
+ local url num
49
+ url="$(gh issue create --repo "$repo" --title "[Effort] · $name" --body "$(_eff_parent_body)")" \
50
+ || { echo "effort_new: failed to create the parent issue" >&2; return 1; }
51
+ num="${url##*/}"
52
+ gh issue edit "$num" --repo "$repo" --title "[Effort] $num · $name" >/dev/null 2>&1
53
+ echo " ✓ effort #$num · $name" >&2
54
+
55
+ local i=0 sub child
56
+ for sub in "$@"; do
57
+ i=$((i + 1))
58
+ child="$(gh issue create --repo "$repo" --title "[Effort $num] $i · $sub" --body "Parent #$num." )" || continue
59
+ effort_link_sub "$num" "${child##*/}" || true
60
+ done
61
+ printf '%s\n' "$num"
62
+ }
63
+
64
+ # effort_start <N> [<slug>] — create the effort/<N> integration branch + its worktree from the base.
65
+ effort_start() {
66
+ _eff_need git || return 1
67
+ local num="${1:-}" slug_override="${2:-}" repo base root title slug branch wt
68
+ [ -n "$num" ] || { echo "effort_start: <effort issue #> required" >&2; return 1; }
69
+ repo="$(_eff_repo)"; base="$(_eff_base)"
70
+ root="$(git worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2; exit}')"
71
+ [ -n "$root" ] || { echo "effort_start: not in a git repo" >&2; return 1; }
72
+
73
+ if [ -n "$slug_override" ]; then slug="$slug_override"
74
+ else
75
+ title="$(gh issue view "$num" --repo "$repo" --json title -q .title 2>/dev/null)"
76
+ slug="$(_eff_slug "${title:-effort}")"; [ -n "$slug" ] || slug="effort"
77
+ fi
78
+ branch="effort/$num-$slug"; wt="$root/.claude/worktrees/effort-$num"
79
+
80
+ git -C "$root" fetch origin "$base" --quiet 2>/dev/null || true
81
+ if git -C "$root" show-ref --verify --quiet "refs/heads/$branch"; then
82
+ echo "effort_start: branch $branch already exists" >&2
83
+ else
84
+ local from="origin/$base"; git -C "$root" rev-parse --verify --quiet "$from" >/dev/null 2>&1 || from="$base"
85
+ git -C "$root" worktree add -b "$branch" "$wt" "$from" >/dev/null 2>&1 \
86
+ || { echo "effort_start: failed to create worktree for $branch" >&2; return 1; }
87
+ fi
88
+ echo " ✓ effort #$num → $branch (worktree: $wt)" >&2
89
+ printf '%s|%s|%s\n' "$wt" "$branch" "$num"
90
+ }
91
+
92
+ # effort_pr [<N>] — open the single PR effort/<N> → base. N defaults to the current effort branch.
93
+ effort_pr() {
94
+ _eff_need gh || return 1
95
+ local num="${1:-}" repo base branch title name
96
+ repo="$(_eff_repo)"; base="$(_eff_base)"
97
+ branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"
98
+ [ -n "$num" ] || num="$(effort_branch_num "$branch")"
99
+ [ -n "$num" ] || { echo "effort_pr: not on an effort/<N>-… branch and no <N> given" >&2; return 1; }
100
+ case "$branch" in effort/"$num"-*) : ;; *) echo "effort_pr: current branch ($branch) is not effort/$num-…" >&2; return 1 ;; esac
101
+
102
+ git push -u origin "$branch" >/dev/null 2>&1 || true
103
+ title="$(gh issue view "$num" --repo "$repo" --json title -q .title 2>/dev/null)"
104
+ name="$(printf '%s' "$title" | sed -E 's/^\[Effort\] [0-9]+ · ?//')"
105
+ gh pr create --repo "$repo" --base "$base" --head "$branch" \
106
+ --title "[Effort] $num · ${name:-effort}" \
107
+ --body "$(printf 'Closes the #%s effort.\n\n## For agents\nSee #%s for the goal, scope, and entry points.\n' "$num" "$num")"
108
+ }
109
+
110
+ # effort_close <N> — snapshot per-sub diffs (before squash), squash-merge the PR, close parent + subs.
111
+ # Destructive: it merges and closes. Snapshots first so the per-sub work record survives the squash.
112
+ effort_close() {
113
+ _eff_need gh || return 1; _eff_need jq || return 1
114
+ local num="${1:-}" repo base branch
115
+ [ -n "$num" ] || { echo "effort_close: <effort issue #> required" >&2; return 1; }
116
+ repo="$(_eff_repo)"; base="$(_eff_base)"
117
+ branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"
118
+ case "$branch" in effort/"$num"-*) : ;; *) echo "effort_close: run from the effort/$num-… branch" >&2; return 1 ;; esac
119
+
120
+ # (a) snapshot the per-sub-issue diffs while the unsquashed history still exists.
121
+ effort_snapshot_subs "$num" "origin/$base" || true
122
+ # (b) squash-merge the effort PR.
123
+ gh pr merge "$branch" --repo "$repo" --squash --delete-branch >/dev/null 2>&1 \
124
+ || { echo "effort_close: could not squash-merge the PR for $branch (open? mergeable?)" >&2; return 1; }
125
+ echo " ✓ merged $branch" >&2
126
+ # (c) close every native sub-issue, then the parent.
127
+ local sub
128
+ for sub in $(gh api "repos/$repo/issues/$num/sub_issues" --jq '.[].number' 2>/dev/null); do
129
+ gh issue close "$sub" --repo "$repo" --reason completed >/dev/null 2>&1 && echo " ✓ closed sub #$sub" >&2
130
+ done
131
+ gh issue close "$num" --repo "$repo" --reason completed >/dev/null 2>&1 && echo " ✓ closed effort #$num" >&2
132
+ }
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env bash
2
+ # effort-plan.sh — the session-fit view (`kit effort plan` when the project ships the kit CLI).
3
+ #
4
+ # Reads the open efforts (label:effort), groups them by their `flow:` tag, orders each flow by the
5
+ # native `blocked_by` edges, and packs the efforts into session-sized batches under a context budget.
6
+ # Answers "which efforts fit in one session before the context window fills, and in what order?".
7
+ #
8
+ # ctx weights: S=1 · M=2 · L=4 · XL=8. The budget (KIT_SESSION_BUDGET, default 4) is how much one
9
+ # session holds. L/XL efforts are flagged "→ delegate subs" — delegating an effort's sub-issues to
10
+ # sub-agents in their own worktrees keeps the MAIN session light (a sub-agent's file reading happens
11
+ # in its own context; only a summary returns), which is the real lever that widens a session
12
+ # (rules/agent-execution-routing.md).
13
+ #
14
+ # Requires: gh, jq. bash 3.2 (no associative arrays — parallel indexed arrays + linear scan; N small).
15
+
16
+ EFFORT_REPO="${EFFORT_REPO:-${KIT_REPO:-}}"
17
+
18
+ _ep_weight() { case "$1" in S) echo 1 ;; M) echo 2 ;; L) echo 4 ;; XL) echo 8 ;; *) echo 2 ;; esac; }
19
+
20
+ effort_plan() {
21
+ command -v gh >/dev/null 2>&1 && command -v jq >/dev/null 2>&1 || { echo "effort plan: needs gh + jq" >&2; return 1; }
22
+ local repo="$EFFORT_REPO" budget="${KIT_SESSION_BUDGET:-4}"
23
+ [[ -n "$repo" ]] || { echo "effort plan: no repo (set KIT_REPO / EFFORT_REPO)" >&2; return 1; }
24
+
25
+ # 1. open efforts + their flow:/ctx: labels (one query).
26
+ local raw
27
+ raw="$(gh issue list --repo "$repo" --label effort --state open --limit 100 \
28
+ --json number,title,labels \
29
+ --jq '.[] | [ (.number|tostring),
30
+ ([.labels[].name|select(startswith("flow:"))|ltrimstr("flow:")]|first // "—"),
31
+ ([.labels[].name|select(startswith("ctx:"))|ltrimstr("ctx:")]|first // "?"),
32
+ .title ] | @tsv' 2>/dev/null)" \
33
+ || { echo "effort plan: gh query failed (check gh auth)" >&2; return 1; }
34
+ [[ -n "$raw" ]] || { echo "effort plan: no open efforts found" >&2; return 0; }
35
+
36
+ # 2. read into parallel arrays + fetch blocked_by per effort (small N).
37
+ local -a nums=() flows=() ctxs=() titles=() blocks=()
38
+ local n f c t
39
+ while IFS=$'\t' read -r n f c t; do
40
+ [[ -n "$n" ]] || continue
41
+ nums+=("$n"); flows+=("$f"); ctxs+=("$c"); titles+=("$t")
42
+ blocks+=("$(gh api "repos/$repo/issues/$n/dependencies/blocked_by" --jq '[.[].number]|join(",")' 2>/dev/null)")
43
+ done <<< "$raw"
44
+
45
+ # 3. distinct flows, first-seen order.
46
+ local -a flowlist=()
47
+ for f in "${flows[@]}"; do
48
+ case " ${flowlist[*]-} " in *" $f "*) ;; *) flowlist+=("$f") ;; esac
49
+ done
50
+
51
+ printf '\n kit effort plan — session budget %s (ctx S=1 M=2 L=4 XL=8 · L/XL → delegate subs)\n' "$budget"
52
+
53
+ local i j b
54
+ for f in "${flowlist[@]}"; do
55
+ printf '\n Flow: %s\n' "$f"
56
+
57
+ # indices in this flow
58
+ local -a idxs=()
59
+ for i in "${!nums[@]}"; do [[ "${flows[$i]}" == "$f" ]] && idxs+=("$i"); done
60
+
61
+ # 4. order: Kahn-lite — emit an effort once all its in-flow blockers are placed (roots first).
62
+ local -a order=()
63
+ local placed=" " progress=1
64
+ while [[ "${#order[@]}" -lt "${#idxs[@]}" && "$progress" -eq 1 ]]; do
65
+ progress=0
66
+ for i in "${idxs[@]}"; do
67
+ case "$placed" in *" $i "*) continue ;; esac
68
+ local ready=1
69
+ for b in ${blocks[$i]//,/ }; do
70
+ for j in "${idxs[@]}"; do
71
+ if [[ "${nums[$j]}" == "$b" ]]; then
72
+ case "$placed" in *" $j "*) ;; *) ready=0 ;; esac
73
+ fi
74
+ done
75
+ done
76
+ if [[ "$ready" -eq 1 ]]; then order+=("$i"); placed="$placed$i "; progress=1; fi
77
+ done
78
+ done
79
+ # cycle / leftover safety: append anything not yet placed.
80
+ for i in "${idxs[@]}"; do case "$placed" in *" $i "*) ;; *) order+=("$i"); placed="$placed$i " ;; esac; done
81
+
82
+ # 5. greedy-pack into sessions: new session when budget would overflow OR the next effort is
83
+ # blocked by one already in the current batch (a blocker and its dependent can't share a session).
84
+ local sess=1 load=0 batch=" "
85
+ printf ' Session %s:\n' "$sess"
86
+ for i in "${order[@]}"; do
87
+ local w; w="$(_ep_weight "${ctxs[$i]}")"
88
+ local neednew=0
89
+ (( load + w > budget )) && neednew=1
90
+ for b in ${blocks[$i]//,/ }; do case "$batch" in *" $b "*) neednew=1 ;; esac; done
91
+ if [[ "$neednew" -eq 1 && "$load" -gt 0 ]]; then
92
+ sess=$((sess + 1)); load=0; batch=" "
93
+ printf ' Session %s:\n' "$sess"
94
+ fi
95
+ load=$((load + w)); batch="$batch${nums[$i]} "
96
+ local note=""; case "${ctxs[$i]}" in L|XL) note=" → delegate subs" ;; esac
97
+ local dep=""; [[ -n "${blocks[$i]}" ]] && dep=" (after #${blocks[$i]//,/, #})"
98
+ # strip the "[Effort] N · " prefix + a leading "[Flow] " tag — #N and the flow are already shown.
99
+ local disp; disp="$(printf '%s' "${titles[$i]}" | sed -E 's/^\[Effort( [0-9]+)?\] [0-9]+ · ?//; s/^\[[A-Za-z]+\] //')"
100
+ printf ' #%-4s [%-2s] %s%s%s\n' "${nums[$i]}" "${ctxs[$i]}" "$disp" "$dep" "$note"
101
+ done
102
+ done
103
+ printf '\n'
104
+ }
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env bash
2
+ # effort.sh — shared git-mechanics for the effort-* skill family (the effort model; rules/effort-model.md).
3
+ #
4
+ # Family 1 of the kit boundary (one bash home per op): consumed by the effort-{new,start,pr,close}
5
+ # skills. The skills are thin callers — no second implementation of these ops anywhere.
6
+ #
7
+ # Functions:
8
+ # effort_link_sub <parent_num> <child_num> link a native GitHub sub-issue (REST sub-issues API)
9
+ # effort_branch_num echo the effort issue # parsed from the current branch
10
+ # effort_trace_dir <parent_num> echo (and mkdir -p) the trace dir under the git-common-dir
11
+ # effort_snapshot_subs <parent_num> [base_ref] snapshot per-sub-issue diffs BEFORE squash (work trace)
12
+ # effort_title_lint <full-title> enforce the concise / no-jargon / flow-tagged title rule
13
+ # effort_set_blocked_by <issue> <blocker> set a native GitHub blocked_by dependency edge
14
+ #
15
+ # Requires: gh, jq, git. bash 3.2 compatible.
16
+
17
+ # Repo resolves from kit.config.json (KIT_REPO) — load it if a caller hasn't already.
18
+ if [[ -z "${KIT_REPO:-}" ]]; then
19
+ _eff_root="$(git rev-parse --show-toplevel 2>/dev/null)"
20
+ if [[ -n "$_eff_root" && -f "$_eff_root/scripts/lib/kit-config.sh" ]]; then
21
+ # shellcheck source=/dev/null
22
+ source "$_eff_root/scripts/lib/kit-config.sh" && load_kit_config >/dev/null 2>&1 || true
23
+ fi
24
+ unset _eff_root
25
+ fi
26
+ EFFORT_REPO="${EFFORT_REPO:-${KIT_REPO:-}}"
27
+
28
+ # Link a child issue as a native GitHub sub-issue of a parent.
29
+ # The sub-issues REST API takes the child's DATABASE id (not its number):
30
+ # POST /repos/{owner}/{repo}/issues/{parent}/sub_issues {"sub_issue_id": <child db id>}
31
+ effort_link_sub() {
32
+ local parent="$1" child="$2" child_id
33
+ [[ -n "$parent" && -n "$child" ]] || { echo "effort_link_sub: parent + child issue numbers required" >&2; return 1; }
34
+ child_id="$(gh api "repos/$EFFORT_REPO/issues/$child" --jq .id 2>/dev/null)" \
35
+ || { echo "effort_link_sub: could not resolve db id for #$child" >&2; return 1; }
36
+ gh api --method POST "repos/$EFFORT_REPO/issues/$parent/sub_issues" \
37
+ -F sub_issue_id="$child_id" >/dev/null 2>&1 \
38
+ && { echo " ✓ linked #$child as sub-issue of #$parent" >&2; return 0; } \
39
+ || { echo " ✗ failed to link #$child under #$parent (already linked? API unavailable?)" >&2; return 1; }
40
+ }
41
+
42
+ # Parse the effort issue number from an `effort/<N>-<slug>` branch. Echoes N (empty if no match).
43
+ effort_branch_num() {
44
+ local branch="${1:-$(git rev-parse --abbrev-ref HEAD 2>/dev/null)}"
45
+ echo "$branch" | sed -nE 's#^effort/([0-9]+)-.*#\1#p'
46
+ }
47
+
48
+ # The durable trace dir for an effort: under the SHARED git-common-dir so it survives worktree prune
49
+ # and is visible from the main checkout (an exporter can consume it later). Echoes the path.
50
+ effort_trace_dir() {
51
+ local parent="$1" common
52
+ common="$(git rev-parse --git-common-dir 2>/dev/null)" || return 1
53
+ case "$common" in /*) : ;; *) common="$(cd "$common" 2>/dev/null && pwd)" || return 1 ;; esac
54
+ local dir="$common/traces/effort-$parent"
55
+ mkdir -p "$dir" 2>/dev/null || return 1
56
+ printf '%s' "$dir"
57
+ }
58
+
59
+ # Snapshot per-sub-issue diffs BEFORE the effort PR is squash-merged (squash collapses the per-sub
60
+ # commit pairs that ARE the per-unit work record — see effort-model.md "Trace hard rules").
61
+ #
62
+ # Strategy (simple, exporter-consumable): for each commit on the effort branch that isn't on the
63
+ # base, write its diff + metadata to $traces/effort-<N>/<seq>-<shortsha>.{diff,meta}. We map a commit
64
+ # to a sub-issue by the LAST "#<num>" reference in its subject/body (the convention: one commit per
65
+ # sub-issue mentioning that sub-issue). Commits with no #ref still get snapshotted (sub="").
66
+ #
67
+ # Args: <parent_num> [base_ref=origin/main]
68
+ effort_snapshot_subs() {
69
+ local parent="$1" base="${2:-origin/main}" branch dir shas seq=0
70
+ [[ -n "$parent" ]] || { echo "effort_snapshot_subs: parent issue number required" >&2; return 1; }
71
+ branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"
72
+ dir="$(effort_trace_dir "$parent")" || { echo "effort_snapshot_subs: could not create trace dir" >&2; return 1; }
73
+
74
+ git fetch origin --quiet 2>/dev/null || true
75
+ # Oldest-first so <seq> increases with history.
76
+ shas="$(git rev-list --reverse "$base..HEAD" 2>/dev/null)"
77
+ if [[ -z "$shas" ]]; then
78
+ echo "effort_snapshot_subs: no commits between $base and HEAD — nothing to snapshot" >&2
79
+ return 0
80
+ fi
81
+
82
+ # An index file ties the snapshot together for an exporter.
83
+ local index="$dir/index.jsonl"
84
+ : > "$index"
85
+
86
+ local sha sub subj body
87
+ for sha in $shas; do
88
+ seq=$((seq + 1))
89
+ subj="$(git log -1 --format='%s' "$sha")"
90
+ body="$(git log -1 --format='%b' "$sha")"
91
+ # Last #<num> mentioned = the sub-issue this commit closes/implements (effort-model convention).
92
+ sub="$(printf '%s\n%s' "$subj" "$body" | grep -oE '#[0-9]+' | tail -1 | tr -d '#')"
93
+ local stub
94
+ stub="$(printf '%02d-%s' "$seq" "$(git rev-parse --short "$sha")")"
95
+ git show --no-color --format=fuller "$sha" > "$dir/$stub.diff" 2>/dev/null
96
+ # meta: commit→sub-issue pairing + outcome hint for the record.
97
+ jq -n --arg parent "$parent" --arg sub "${sub:-}" --arg sha "$sha" \
98
+ --arg subject "$subj" --arg branch "$branch" --arg file "$stub.diff" \
99
+ '{parent:($parent|tonumber), sub_issue:(if $sub=="" then null else ($sub|tonumber) end),
100
+ commit:$sha, subject:$subject, branch:$branch, diff_file:$file, outcome:"merged"}' \
101
+ > "$dir/$stub.meta"
102
+ cat "$dir/$stub.meta" >> "$index"
103
+ done
104
+
105
+ echo " ✓ snapshotted $seq commit(s) to $dir (index.jsonl)" >&2
106
+ printf '%s' "$dir"
107
+ }
108
+
109
+ # ── Effort-title rule — concise, jargon-free, flow-tagged ──────────────────────────────────────────
110
+ # The board must say what an effort delivers and which FLOW it belongs to. So the `<Name>` part of an
111
+ # effort/sub title is a short plain-language outcome with an optional leading `[Flow]` tag from a
112
+ # controlled vocabulary — never internal jargon, glyphs, code identifiers, parentheticals, em-dash
113
+ # sub-clauses, or >6 words (detail goes in the body). See rules/effort-model.md § Titles.
114
+
115
+ # Controlled flow vocabulary for the optional leading [Flow] title tag. Projects override via EFFORT_FLOWS.
116
+ EFFORT_FLOWS="${EFFORT_FLOWS:-Core UI API Docs Infra Auth Data Web App}"
117
+ # Jargon denylist — internal terms that read as noise on the board. Override via EFFORT_TITLE_JARGON.
118
+ EFFORT_TITLE_JARGON="${EFFORT_TITLE_JARGON:-chrome seam contract claim rescue stash teardown scaffolding shim boilerplate refactor wiring}"
119
+
120
+ # effort_title_lint <full-title> — 0 if the title is clean; 1 + reasons on stderr otherwise.
121
+ # Accepts "[Effort] N · [Flow] short name" or "[Effort N] M · short name".
122
+ effort_title_lint() {
123
+ local title="$1" name flow rest
124
+ local -a reasons=()
125
+ [[ -n "$title" ]] || { echo "effort_title_lint: title required" >&2; return 2; }
126
+
127
+ # 1. peel the structural prefix → the human name part. parent: "[Effort] N · " sub: "[Effort N] M · "
128
+ name="$(printf '%s' "$title" | sed -E 's/^\[Effort( [0-9]+)?\] [0-9]+ · ?//')"
129
+ [[ "$name" == "$title" ]] && reasons+=("missing the '[Effort] N · ' / '[Effort N] M · ' prefix")
130
+
131
+ # 2. peel one optional leading [Flow] tag and validate it against the controlled vocabulary.
132
+ if [[ "$name" =~ ^\[([A-Za-z]+)\]\ (.+)$ ]]; then
133
+ flow="${BASH_REMATCH[1]}"; rest="${BASH_REMATCH[2]}"; name="$rest"
134
+ case " $EFFORT_FLOWS " in
135
+ *" $flow "*) : ;;
136
+ *) reasons+=("flow tag [$flow] is not in the vocabulary ($EFFORT_FLOWS)") ;;
137
+ esac
138
+ fi
139
+
140
+ # 3. the remaining name must be a concise, plain outcome phrase.
141
+ case "$name" in *"("*|*")"*) reasons+=("no parentheses — detail goes in the body") ;; esac
142
+ case "$name" in *" — "*|*" · "*|*" / "*) reasons+=("no ' — ' / ' · ' / ' / ' sub-clauses — one clear phrase") ;; esac
143
+ case "$name" in *—*|*▾*|*▸*|*→*|*✓*|*✗*|*…*|*•*) reasons+=("no glyphs (— ▾ ▸ → ✓ ✗ … •) in the title") ;; esac
144
+ # code identifiers: file extensions, snake_case, or a code-dir path.
145
+ if printf '%s' "$name" | grep -qE '\.(sh|ts|tsx|js|mjs|json|css|md|ya?ml)([^A-Za-z0-9]|$)|[a-z0-9]+_[a-z0-9]+|(^|[^A-Za-z])(scripts|apps|packages|src)/'; then
146
+ reasons+=("no code identifiers (file names, paths, snake_case) — name the outcome")
147
+ fi
148
+ # jargon denylist (word-boundary, case-insensitive).
149
+ local w
150
+ for w in $EFFORT_TITLE_JARGON; do
151
+ printf '%s' "$name" | grep -qiwE "$w" && reasons+=("jargon word '$w' — use plain language")
152
+ done
153
+ # word count ≤ 6.
154
+ local wc; wc=$(printf '%s\n' "$name" | wc -w | tr -d ' ')
155
+ [[ "$wc" -gt 6 ]] && reasons+=("name is $wc words — keep it ≤ 6 (detail goes in the body)")
156
+
157
+ if [[ "${#reasons[@]}" -gt 0 ]]; then
158
+ { echo "✗ effort title fails the concise / no-jargon rule: $title"
159
+ for w in "${reasons[@]}"; do echo " - $w"; done
160
+ } >&2
161
+ return 1
162
+ fi
163
+ return 0
164
+ }
165
+
166
+ # effort_set_blocked_by <issue> <blocker-issue> — declare the native GitHub dependency "<issue> is
167
+ # blocked_by <blocker>" (the edge GitHub renders on the board). The API takes the blocker's DB id.
168
+ effort_set_blocked_by() {
169
+ local issue="$1" blocker="$2" bid
170
+ [[ -n "$issue" && -n "$blocker" ]] || { echo "effort_set_blocked_by: <issue> <blocker> required" >&2; return 1; }
171
+ bid="$(gh api "repos/$EFFORT_REPO/issues/$blocker" --jq .id 2>/dev/null)" \
172
+ || { echo "effort_set_blocked_by: could not resolve db id for #$blocker" >&2; return 1; }
173
+ gh api --method POST "repos/$EFFORT_REPO/issues/$issue/dependencies/blocked_by" -F issue_id="$bid" >/dev/null 2>&1 \
174
+ && { echo " ✓ #$issue blocked_by #$blocker" >&2; return 0; } \
175
+ || { echo " ✗ failed to set #$issue blocked_by #$blocker (already set? API unavailable?)" >&2; return 1; }
176
+ }
177
+
178
+ # Self-test: `bash scripts/lib/effort.sh --selftest` exercises effort_title_lint (jargon → reject,
179
+ # plain → accept). Runs only when executed directly.
180
+ if [[ "${BASH_SOURCE[0]:-}" == "${0:-}" && "${1:-}" == "--selftest" ]]; then
181
+ _et_rc=0
182
+ _et_fail() { if effort_title_lint "$1" >/dev/null 2>&1; then echo "FAIL expected-reject passed: $1"; _et_rc=1; else echo "ok reject: $1"; fi; }
183
+ _et_pass() { if effort_title_lint "$1" >/dev/null 2>&1; then echo "ok accept: $1"; else echo "FAIL expected-accept rejected: $1"; effort_title_lint "$1"; _et_rc=1; fi; }
184
+ _et_fail "[Effort] 12 · operator chrome — Settings ▾ dropdown + layout fixes"
185
+ _et_fail "[Effort 12] 4 · Section tables — columns + joins (contract B)"
186
+ _et_fail "[Effort] 12 · refactor scripts/kit + quote-aware free-prompt args"
187
+ _et_pass "[Effort] 12 · [UI] operator navigation"
188
+ _et_pass "[Effort] 12 · effort planning conventions"
189
+ _et_pass "[Effort 12] 4 · module tables"
190
+ exit "$_et_rc"
191
+ fi
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env bash
2
+ # engine-adapter.sh — pluggable bridge from the kit CLI to the Plan Engine.
3
+ #
4
+ # Family 2 of kit-engine-boundary.md: plan/ticket/Deliverable graph ops. The kit CLI is the
5
+ # human/Claude surface; the Plan Engine Postgres graph (ADR-004/005) is the source of truth.
6
+ # This adapter is the seam between them — and it is PLUGGABLE so the kit stays portable as a
7
+ # product: the default mode is "off", meaning the kit runs engine-less (bash + GitHub, today's
8
+ # behavior). cckit and clients opt in by setting the engine block in .claude/kit.config.json.
9
+ #
10
+ # Config (.claude/kit.config.json):
11
+ # "engine": { "mode": "off" | "http", "url": "https://engine.example", "token_env": "CCKIT_ENGINE_TOKEN" }
12
+ #
13
+ # Source it, then call engine_cmd / engine_call. Requires: jq, curl (only when mode != off).
14
+
15
+ _engine_cfg() { echo "${KIT_CONFIG:-.claude/kit.config.json}"; }
16
+
17
+ engine_mode() { jq -r '.engine.mode // "off"' "$(_engine_cfg)" 2>/dev/null || echo off; }
18
+ engine_url() { jq -r '.engine.url // ""' "$(_engine_cfg)" 2>/dev/null; }
19
+ engine_enabled() { [[ "$(engine_mode)" != "off" ]]; }
20
+
21
+ # engine_call <METHOD> <path> [json-body] — returns the engine response on stdout.
22
+ # Returns 3 when the engine is not configured (local mode), so callers can fall back to GitHub.
23
+ engine_call() {
24
+ # NOTE: `route` not `path` — `path` is a special zsh parameter tied to $PATH; a `local path=…`
25
+ # silently corrupts the command search path under zsh (the session shell), so jq/curl then fail
26
+ # and engine_enabled reads "off". (delegation-brief: status/path are zsh-special.)
27
+ local method="$1" route="$2" body="${3:-}" url tokenvar tok
28
+ engine_enabled || { echo "engine: off (local mode)" >&2; return 3; }
29
+ url="$(engine_url)"; [[ -n "$url" ]] || { echo "engine.url not set in $(_engine_cfg)" >&2; return 3; }
30
+ command -v curl >/dev/null || { echo "curl required for engine mode" >&2; return 1; }
31
+ tokenvar="$(jq -r '.engine.token_env // ""' "$(_engine_cfg)")"
32
+ local -a auth=()
33
+ # Portable indirect expansion — bash's ${!var} is not zsh-compatible (zsh would need ${(P)var}).
34
+ [[ -n "$tokenvar" ]] && eval "tok=\${$tokenvar:-}"
35
+ # Auto-load the engine token from the untracked local secret file when the env var is unset.
36
+ # The token lives in <main-checkout>/scripts/.engine-secret.env (gitignored plaintext) and nothing
37
+ # else sources it — without this the Authorization header silently drops and effort-metrics sync
38
+ # buffers instead of POSTing. The secret is UNTRACKED, so it exists ONLY in the MAIN checkout, not
39
+ # in effort worktrees (where kit-effort-close actually runs). Resolve it via git-common-dir — the
40
+ # shared .git whose parent IS the main checkout, correct from any worktree — then fall back to the
41
+ # current checkout toplevel / this script's location. Missing file = silent no-op. zsh-safe: no
42
+ # `path` local, portable indirect expansion (no ${!var}).
43
+ if [[ -z "${tok:-}" && -n "$tokenvar" ]]; then
44
+ local common main_root secret_file="" cand
45
+ # 1) main checkout via the shared git-common-dir (works from any worktree).
46
+ common="$(git rev-parse --git-common-dir 2>/dev/null)"
47
+ if [[ -n "$common" ]]; then
48
+ case "$common" in /*) : ;; *) common="$PWD/$common" ;; esac
49
+ main_root="$(dirname "$common")"
50
+ [[ -f "$main_root/scripts/.engine-secret.env" ]] && secret_file="$main_root/scripts/.engine-secret.env"
51
+ fi
52
+ # 2) current checkout toplevel, then 3) relative to this script — fallbacks.
53
+ if [[ -z "$secret_file" ]]; then
54
+ for cand in "$(git rev-parse --show-toplevel 2>/dev/null)" \
55
+ "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../.." 2>/dev/null && pwd)"; do
56
+ [[ -n "$cand" && -f "$cand/scripts/.engine-secret.env" ]] && { secret_file="$cand/scripts/.engine-secret.env"; break; }
57
+ done
58
+ fi
59
+ if [[ -n "$secret_file" ]]; then
60
+ # shellcheck disable=SC1090
61
+ . "$secret_file"
62
+ eval "tok=\${$tokenvar:-}"
63
+ fi
64
+ fi
65
+ [[ -n "${tok:-}" ]] && auth=(-H "Authorization: Bearer $tok")
66
+ if [[ -n "$body" ]]; then
67
+ curl -fsS -X "$method" "${auth[@]}" -H "Content-Type: application/json" --data "$body" "$url$route"
68
+ else
69
+ curl -fsS -X "$method" "${auth[@]}" -H "Content-Type: application/json" "$url$route"
70
+ fi
71
+ }
72
+
73
+ # kit engine <status|ping>
74
+ engine_cmd() {
75
+ case "${1:-status}" in
76
+ status)
77
+ local mode; mode="$(engine_mode)"
78
+ if engine_enabled; then
79
+ echo "engine mode: $mode"
80
+ echo "engine url: $(engine_url)"
81
+ else
82
+ echo "engine mode: off (local mode) - graph ops are GitHub-backed"
83
+ echo "connect: set .engine in $(_engine_cfg) (see scripts/lib/engine-adapter.sh)"
84
+ fi
85
+ ;;
86
+ ping)
87
+ engine_enabled || { echo "engine: off (local mode) - nothing to ping"; return 0; }
88
+ engine_call GET /health && echo "" || { echo "engine unreachable" >&2; return 1; }
89
+ ;;
90
+ *) echo "unknown: kit engine ${1:-}" >&2; return 2 ;;
91
+ esac
92
+ }