harveyz-skill 0.2.1 → 0.3.0
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/README.md +94 -15
- package/bin/cli.js +12 -2
- package/lib/bundles.js +16 -0
- package/package.json +2 -1
- package/skills/harness/git-workflow-init/SKILL.md +321 -0
- package/skills/harness/git-workflow-init/evals/evals.json +104 -0
- package/skills/harness/git-workflow-init/references/git-workflow-template.md +94 -0
- package/skills/harness/git-workflow-init/references/hook-templates.md +232 -0
- package/skills/harness/git-workflow-init/references/lock-file-format.md +34 -0
- package/skills/harness/git-workflow-init/references/render_docs.py +261 -0
- package/skills/harness/git-workflow-init/references/workflow-config.yml +99 -0
- package/skills-index.json +2 -1
- package/tools/p-launch/p-launch.sh +32 -7
- package/tools/p-launch/tests/unit.bats +78 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# workflow-config.yml
|
|
2
|
+
# Git 工作流配置文件 — 由 /git-workflow-init skill 读取、审核并部署
|
|
3
|
+
#
|
|
4
|
+
# 使用方式:将此文件复制到项目根目录,按需修改后运行 /git-workflow-init
|
|
5
|
+
#
|
|
6
|
+
# 每个节对应一个 git hook,注释掉整节 = 不安装该 hook
|
|
7
|
+
|
|
8
|
+
# ── 元信息 ────────────────────────────────────────────────────────────────────
|
|
9
|
+
meta:
|
|
10
|
+
name: "my-project-workflow"
|
|
11
|
+
version: "1.0.0"
|
|
12
|
+
# preset 决定各节的默认值,可在下方局部覆盖
|
|
13
|
+
# 可选值:gitflow | github-flow | trunk-based | custom
|
|
14
|
+
preset: gitflow
|
|
15
|
+
|
|
16
|
+
# ── 分支拓扑与保护(生成 pre-commit hook)────────────────────────────────────
|
|
17
|
+
#
|
|
18
|
+
# allow_direct_commit: false → 直接提交时 exit 1(硬拦截)
|
|
19
|
+
# allow_direct_commit: true → 直接提交时仅打印警告,不阻断
|
|
20
|
+
# merge_from 使用 shell glob 语法(* 匹配任意字符,不含 /)
|
|
21
|
+
branches:
|
|
22
|
+
protected:
|
|
23
|
+
- name: main
|
|
24
|
+
allow_direct_commit: false
|
|
25
|
+
merge_from:
|
|
26
|
+
- staging
|
|
27
|
+
- "release/*"
|
|
28
|
+
|
|
29
|
+
- name: staging
|
|
30
|
+
allow_direct_commit: false
|
|
31
|
+
merge_from:
|
|
32
|
+
- "feature/*"
|
|
33
|
+
- "fix/*"
|
|
34
|
+
- "chore/*"
|
|
35
|
+
- "doc/*"
|
|
36
|
+
|
|
37
|
+
# ── 分支命名规范(生成 post-checkout hook)───────────────────────────────────
|
|
38
|
+
#
|
|
39
|
+
# enforce: true → 切换到不合规分支时打印警告(advisory,不阻断)
|
|
40
|
+
# enforce: false → 跳过,不安装此 hook
|
|
41
|
+
# allowed_patterns 使用 POSIX ERE 语法
|
|
42
|
+
branch_naming:
|
|
43
|
+
enforce: true
|
|
44
|
+
allowed_patterns:
|
|
45
|
+
- "^feature/.+"
|
|
46
|
+
- "^fix/.+"
|
|
47
|
+
- "^chore/.+"
|
|
48
|
+
- "^doc/.+"
|
|
49
|
+
- "^release/[0-9]+\\.[0-9]+\\.[0-9]+(-.+)?$"
|
|
50
|
+
exempt: # 永远豁免命名检查的分支
|
|
51
|
+
- main
|
|
52
|
+
- staging
|
|
53
|
+
|
|
54
|
+
# ── 提交信息格式(生成 commit-msg hook)──────────────────────────────────────
|
|
55
|
+
#
|
|
56
|
+
# format 可选值:
|
|
57
|
+
# conventional → 使用下方 conventional 节的设置
|
|
58
|
+
# regex → 使用自定义 pattern 字段(需同时填写 pattern)
|
|
59
|
+
# none → 不检查格式(但若 enforce: true 会安装空 hook)
|
|
60
|
+
commit_message:
|
|
61
|
+
enforce: true
|
|
62
|
+
format: conventional
|
|
63
|
+
# format: regex 时启用此行:
|
|
64
|
+
# pattern: "^(JIRA-\\d+|NOJIRA): .{1,80}"
|
|
65
|
+
conventional:
|
|
66
|
+
types:
|
|
67
|
+
- feat
|
|
68
|
+
- fix
|
|
69
|
+
- chore
|
|
70
|
+
- docs
|
|
71
|
+
- refactor
|
|
72
|
+
- test
|
|
73
|
+
- style
|
|
74
|
+
- perf
|
|
75
|
+
require_scope: false # true = feat(scope): msg 中 scope 必填
|
|
76
|
+
max_subject_length: 80
|
|
77
|
+
|
|
78
|
+
# ── Tag 命名规范(生成 pre-push hook 的 tag 段)───────────────────────────────
|
|
79
|
+
#
|
|
80
|
+
# push_guard: true → 推送不合规 tag 时 exit 1
|
|
81
|
+
# allowed_patterns 使用 POSIX ERE 语法,满足其中一个即通过
|
|
82
|
+
# require_annotated: true → 强制使用 git tag -a,拒绝 lightweight tag
|
|
83
|
+
tags:
|
|
84
|
+
enforce: true
|
|
85
|
+
push_guard: true
|
|
86
|
+
allowed_patterns:
|
|
87
|
+
- "^v[0-9]+\\.[0-9]+\\.[0-9]+$" # 正式版:v1.2.3
|
|
88
|
+
- "^v[0-9]+\\.[0-9]+\\.[0-9]+-.+$" # 预发布:v1.2.3-beta.1、v1.2.3-rc.2
|
|
89
|
+
require_annotated: true
|
|
90
|
+
|
|
91
|
+
# ── 推送限制(生成 pre-push hook 的分支段)───────────────────────────────────
|
|
92
|
+
#
|
|
93
|
+
# block_force_push 列出的分支,force push 时 exit 1
|
|
94
|
+
# enforce: false 时整节跳过,不安装 pre-push hook(除非 tags 节已启用)
|
|
95
|
+
push_rules:
|
|
96
|
+
enforce: true
|
|
97
|
+
block_force_push:
|
|
98
|
+
- main
|
|
99
|
+
- staging
|
package/skills-index.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"bundleMeta": {
|
|
9
9
|
"analysis": "分析工具(skill-analyzer)",
|
|
10
10
|
"brainstorming": "设计与规划工具(brainstorming + writing-plans)",
|
|
11
|
-
"dev": "开发工作流(executing-plans + systematic-debugging + using-git-worktrees)",
|
|
11
|
+
"dev": "开发工作流(executing-plans + systematic-debugging + using-git-worktrees + git-workflow-init)",
|
|
12
12
|
"document": "文档工具(diataxis-docs)",
|
|
13
13
|
"harness": "测试工具(full-stack-debug-env)",
|
|
14
14
|
"task": "任务管理(pm-task-dispatch + task-close)",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
{ "path": "harness/diataxis-docs", "bundle": "document" },
|
|
21
21
|
{ "path": "harness/full-stack-debug-env", "bundle": "harness" },
|
|
22
22
|
{ "path": "superpowers-fork/brainstorming", "bundle": "brainstorming" },
|
|
23
|
+
{ "path": "harness/git-workflow-init", "bundle": "dev" },
|
|
23
24
|
{ "path": "superpowers-fork/executing-plans", "bundle": "dev" },
|
|
24
25
|
{ "path": "superpowers-fork/systematic-debugging", "bundle": "dev" },
|
|
25
26
|
{ "path": "superpowers-fork/using-git-worktrees", "bundle": "dev" },
|
|
@@ -68,18 +68,43 @@ _launch() {
|
|
|
68
68
|
local path="$1"
|
|
69
69
|
local name="${path:t}"
|
|
70
70
|
local display="${path/$HOME/~}"
|
|
71
|
-
local cursor_ok=false ghostty_ok=false
|
|
71
|
+
local cursor_ok=false ghostty_ok=false ghostty_err="not installed"
|
|
72
72
|
|
|
73
|
-
# Cursor IDE
|
|
73
|
+
# Cursor IDE — CLI first, fall back to .app
|
|
74
74
|
if command -v cursor &>/dev/null; then
|
|
75
75
|
cursor "$path" &>/dev/null &
|
|
76
76
|
cursor_ok=true
|
|
77
|
+
elif [[ -d "/Applications/Cursor.app" ]]; then
|
|
78
|
+
/usr/bin/open -na "Cursor" --args "$path" && cursor_ok=true
|
|
77
79
|
fi
|
|
78
80
|
|
|
79
|
-
# Ghostty —
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
# Ghostty — find app via Spotlight (handles /Applications, ~/Applications,
|
|
82
|
+
# or any custom install path), then invoke "New Ghostty Window Here" service
|
|
83
|
+
# via NSPerformService — same code path as Finder right-click service.
|
|
84
|
+
local ghostty_app
|
|
85
|
+
ghostty_app=$(mdfind "kMDItemCFBundleIdentifier == 'com.mitchellh.ghostty'" 2>/dev/null | /usr/bin/head -1)
|
|
86
|
+
if [[ -z "$ghostty_app" ]]; then
|
|
87
|
+
# mdfind can miss apps before first Spotlight index; fall back to known paths
|
|
88
|
+
for _p in "/Applications/Ghostty.app" "$HOME/Applications/Ghostty.app"; do
|
|
89
|
+
[[ -d "$_p" ]] && { ghostty_app="$_p"; break }
|
|
90
|
+
done
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
if [[ -n "$ghostty_app" ]]; then
|
|
94
|
+
ghostty_err="failed to open"
|
|
95
|
+
# Ghostty's service handler does dirname on the path, so pass a child of
|
|
96
|
+
# the target dir — dirname(target/child) == target.
|
|
97
|
+
local _child service_path
|
|
98
|
+
_child=$(/bin/ls -1A "$path" 2>/dev/null | /usr/bin/head -1)
|
|
99
|
+
service_path="${path}/${_child}"
|
|
100
|
+
${_OSASCRIPT:-/usr/bin/osascript} 2>/dev/null <<OSASCRIPT && ghostty_ok=true
|
|
101
|
+
use framework "AppKit"
|
|
102
|
+
use scripting additions
|
|
103
|
+
set thePboard to current application's NSPasteboard's generalPasteboard()
|
|
104
|
+
thePboard's clearContents()
|
|
105
|
+
thePboard's setPropertyList:{"${service_path}"} forType:"NSFilenamesPboardType"
|
|
106
|
+
return current application's NSPerformService("New Ghostty Window Here", thePboard)
|
|
107
|
+
OSASCRIPT
|
|
83
108
|
fi
|
|
84
109
|
|
|
85
110
|
# ── Launch Report ──────────────────────────────────────────────────────────
|
|
@@ -96,7 +121,7 @@ _launch() {
|
|
|
96
121
|
if $ghostty_ok; then
|
|
97
122
|
printf " ${C[gr]}✓${C[rs]} Ghostty new window opened\n"
|
|
98
123
|
else
|
|
99
|
-
printf " ${C[yl]}⚠${C[rs]} Ghostty
|
|
124
|
+
printf " ${C[yl]}⚠${C[rs]} Ghostty %s\n" "$ghostty_err"
|
|
100
125
|
fi
|
|
101
126
|
|
|
102
127
|
printf '\n'
|
|
@@ -6,7 +6,8 @@ setup() {
|
|
|
6
6
|
SCRIPT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)/p-launch.sh"
|
|
7
7
|
TEST_DIR="$(mktemp -d)"
|
|
8
8
|
MOCK_HOME="${TEST_DIR}/home"
|
|
9
|
-
|
|
9
|
+
MOCK_BIN="${TEST_DIR}/bin"
|
|
10
|
+
mkdir -p "${MOCK_HOME}" "${MOCK_BIN}"
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
teardown() {
|
|
@@ -149,3 +150,79 @@ _src() {
|
|
|
149
150
|
[ "$status" -eq 0 ]
|
|
150
151
|
[ "${#lines[@]}" -eq 2 ]
|
|
151
152
|
}
|
|
153
|
+
|
|
154
|
+
# ── _launch: Cursor ────────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
_launch_src() {
|
|
157
|
+
local code="$1"
|
|
158
|
+
# _OSASCRIPT lets tests inject a mock without relying on PATH,
|
|
159
|
+
# since p-launch.sh calls /usr/bin/osascript via full path.
|
|
160
|
+
local mock_osascript="${MOCK_BIN}/osascript"
|
|
161
|
+
local osascript_override=""
|
|
162
|
+
[[ -x "$mock_osascript" ]] && osascript_override="export _OSASCRIPT='${mock_osascript}'"
|
|
163
|
+
zsh -c "
|
|
164
|
+
export _P_LAUNCH_TEST=1
|
|
165
|
+
export HOME='${MOCK_HOME}'
|
|
166
|
+
export PATH='${MOCK_BIN}:${PATH}'
|
|
167
|
+
${osascript_override}
|
|
168
|
+
source '${SCRIPT}'
|
|
169
|
+
${code}
|
|
170
|
+
"
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@test "_launch: reports ✓ Cursor when cursor CLI is in PATH" {
|
|
174
|
+
printf '#!/bin/sh\nexit 0\n' > "${MOCK_BIN}/cursor"
|
|
175
|
+
chmod +x "${MOCK_BIN}/cursor"
|
|
176
|
+
|
|
177
|
+
run _launch_src "_launch '${TEST_DIR}'"
|
|
178
|
+
[ "$status" -eq 0 ]
|
|
179
|
+
[[ "$output" == *"✓"*"Cursor"* ]]
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@test "_launch: reports ⚠ Cursor when CLI absent and Cursor.app not found" {
|
|
183
|
+
[[ ! -d "/Applications/Cursor.app" ]] || skip "Cursor.app installed — cannot test absence"
|
|
184
|
+
|
|
185
|
+
run _launch_src "_launch '${TEST_DIR}'"
|
|
186
|
+
[ "$status" -eq 0 ]
|
|
187
|
+
[[ "$output" == *"⚠"*"Cursor"* ]]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# ── _launch: Ghostty ───────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
@test "_launch: reports ✓ Ghostty when osascript succeeds" {
|
|
193
|
+
[[ -d "/Applications/Ghostty.app" ]] || skip "Ghostty.app not installed"
|
|
194
|
+
printf '#!/bin/sh\nexit 0\n' > "${MOCK_BIN}/osascript"
|
|
195
|
+
chmod +x "${MOCK_BIN}/osascript"
|
|
196
|
+
|
|
197
|
+
run _launch_src "_launch '${TEST_DIR}'"
|
|
198
|
+
[ "$status" -eq 0 ]
|
|
199
|
+
[[ "$output" == *"✓"*"Ghostty"* ]]
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@test "_launch: reports ⚠ Ghostty when osascript fails" {
|
|
203
|
+
[[ -d "/Applications/Ghostty.app" ]] || skip "Ghostty.app not installed"
|
|
204
|
+
printf '#!/bin/sh\nexit 1\n' > "${MOCK_BIN}/osascript"
|
|
205
|
+
chmod +x "${MOCK_BIN}/osascript"
|
|
206
|
+
|
|
207
|
+
run _launch_src "_launch '${TEST_DIR}'"
|
|
208
|
+
[ "$status" -eq 0 ]
|
|
209
|
+
[[ "$output" == *"⚠"*"Ghostty"* ]]
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
@test "_launch: reports ⚠ Ghostty when Ghostty.app not found" {
|
|
213
|
+
[[ ! -d "/Applications/Ghostty.app" ]] || skip "Ghostty.app installed — cannot test absence"
|
|
214
|
+
|
|
215
|
+
run _launch_src "_launch '${TEST_DIR}'"
|
|
216
|
+
[ "$status" -eq 0 ]
|
|
217
|
+
[[ "$output" == *"⚠"*"Ghostty"* ]]
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@test "_launch: osascript receives working directory in script" {
|
|
221
|
+
[[ -d "/Applications/Ghostty.app" ]] || skip "Ghostty.app not installed"
|
|
222
|
+
printf '#!/bin/sh\nprintf "ARGS:%%s\n" "$@"\nexit 0\n' \
|
|
223
|
+
> "${MOCK_BIN}/osascript"
|
|
224
|
+
chmod +x "${MOCK_BIN}/osascript"
|
|
225
|
+
|
|
226
|
+
run _launch_src "_launch '${TEST_DIR}'"
|
|
227
|
+
[[ "$output" == *"${TEST_DIR}"* ]]
|
|
228
|
+
}
|