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