harveyz-skill 0.23.0 → 0.25.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/CHANGELOG.md +29 -0
- package/bin/cli.js +121 -37
- package/bin/preview.mjs +34 -3
- package/package.json +4 -1
- package/skills/coding/explain-pm/SKILL.md +34 -0
- package/skills/coding/init-workflow/references/acceptance-test.md +21 -6
- package/skills/coding/init-workflow/references/hook-templates.md +22 -1
- package/skills/coding/question-me/SKILL.md +1 -1
- package/skills/coding/rephrase/SKILL.md +25 -0
- package/skills/research/extract-url/SKILL.md +21 -21
- package/skills/research/extract-url/references/__pycache__/article_utils.cpython-314.pyc +0 -0
- package/skills/research/extract-url/references/article_utils.py +35 -25
- package/skills/research/extract-url/references/file-format.md +9 -4
- package/skills/research/extract-url/references/subagent1-fetch-prompt.md +10 -1
- package/skills/research/extract-url/references/subagent2-tag-translate-prompt.md +14 -10
- package/skills/research/extract-url/scripts/__pycache__/config.cpython-314.pyc +0 -0
- package/skills/research/extract-url/scripts/__pycache__/migrate_to_folder_structure.cpython-314.pyc +0 -0
- package/skills/research/extract-url/scripts/config.py +31 -1
- package/skills/research/extract-url/scripts/dedup_check.py +15 -33
- package/skills/research/extract-url/scripts/migrate_to_folder_structure.py +459 -0
- package/skills/research/extract-url/scripts/playwright_web.py +16 -20
- package/skills/research/extract-url/scripts/playwright_web_arxiv.py +284 -0
- package/skills/research/extract-url/scripts/playwright_xcom.py +16 -20
- package/skills/research/extract-url/scripts/validate_article.py +9 -10
- package/skills/research/extract-url/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_article_utils_meta.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_article_utils_tags.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_config.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_count_article_stats.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_dedup_check.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_migrate_to_folder_structure.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_playwright_web.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_playwright_web_arxiv.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_playwright_xcom.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_subagent1_prompt.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_subagent2_prompt.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/__pycache__/test_validate_article.cpython-314-pytest-9.0.2.pyc +0 -0
- package/skills/research/extract-url/tests/conftest.py +15 -20
- package/skills/research/extract-url/tests/test_article_utils_meta.py +84 -0
- package/skills/research/extract-url/tests/test_config.py +47 -0
- package/skills/research/extract-url/tests/test_dedup_check.py +34 -28
- package/skills/research/extract-url/tests/test_migrate_to_folder_structure.py +492 -0
- package/skills/research/extract-url/tests/test_playwright_web.py +55 -18
- package/skills/research/extract-url/tests/test_playwright_web_arxiv.py +157 -0
- package/skills/research/extract-url/tests/test_playwright_xcom.py +8 -0
- package/skills/research/extract-url/tests/test_subagent1_prompt.py +15 -0
- package/skills/research/extract-url/tests/test_subagent2_prompt.py +22 -0
- package/skills/research/extract-url/tests/test_validate_article.py +95 -12
- package/skills/research/pdf-math-translate/SKILL.md +209 -0
- package/skills-index.json +28 -8
- package/tools/hub/docs/design-previews/git-textual-tui-design.html +0 -359
- package/tools/hub/docs/design-previews/tasks-textual-tui-design.html +0 -371
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.25.0] - 2026-07-20
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `hskill` install menu:新增按平台切换的循环视图(per-platform cycling view),一键升级快捷键
|
|
14
|
+
- `pdf-math-translate`:重写 frontmatter,补充 Init/Execute 两阶段流程与最新 CLI 参考
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- `explain-pm`:警示性意见调整为统一放在结尾,不再穿插
|
|
18
|
+
- `init-workflow`:修正 force-push 检测逻辑,合并提交(merge commit)豁免检查
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- 记录不新增 meta.json 完整性检查的 ADR 决策
|
|
22
|
+
|
|
23
|
+
## [0.24.0] - 2026-07-18
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- `extract-url`:用 per-article `meta.json` 索引替代 SQLite 索引,`article_utils`/`dedup_check`/`playwright_web(_arxiv)`/`playwright_xcom`/`validate_article` 全部切换到 meta.json 读写;提供旧数据迁移脚本(写 meta.json + 清理遗留文件)
|
|
27
|
+
- `pdf-math-translate`:从其他项目贡献到 `research` bundle 的 PDF 数学翻译 skill
|
|
28
|
+
- `explain-pm`:新增 skill,注册到 `coding` bundle
|
|
29
|
+
- `rephrase`:新增 skill,单次改写用户陈述以提升精确度
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- `extract-url`:放宽翻译文章的 author/publish_date 校验规则
|
|
33
|
+
- `question-me`:补充决策树格式中 label 字段的示例与一致性规则(v3.0.1)
|
|
34
|
+
- `publish-skill` 审计:修正 `learn-skill`/`survey-skillrepo`/`init-skill` 三个 skill 历史遗留的 contentHash 记录错误(内容本身未变更)
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- `extract-url`:SKILL.md 与 subagent prompt 更新以适配 meta.json 索引;skills-index.json 同步更新 contentHash
|
|
38
|
+
|
|
10
39
|
## [0.23.0] - 2026-07-09
|
|
11
40
|
|
|
12
41
|
### Added
|
package/bin/cli.js
CHANGED
|
@@ -802,15 +802,34 @@ function requireFzf() {
|
|
|
802
802
|
}
|
|
803
803
|
}
|
|
804
804
|
|
|
805
|
-
|
|
806
|
-
function
|
|
807
|
-
|
|
808
|
-
|
|
805
|
+
const G = '\x1b[32m', Y = '\x1b[33m', C = '\x1b[36m', D = '\x1b[2m', R = '\x1b[0m'
|
|
806
|
+
function colorIcon(status) {
|
|
807
|
+
if (status === 'up-to-date') return G + '✓' + R
|
|
808
|
+
if (status === 'update') return Y + '↑' + R
|
|
809
|
+
return D + '—' + R
|
|
810
|
+
}
|
|
811
|
+
function scopeHint(installScope) {
|
|
812
|
+
if (installScope === 'essential') return Y + '»' + R
|
|
813
|
+
if (installScope === 'global' || installScope === 'project') return C + '▸' + R
|
|
814
|
+
return D + '—' + R
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ── Hidden: fzf reload helper ─────────────────────────────────────────────────
|
|
818
|
+
// 供 fzf 的 reload()/execute() 动作回调,重新打印某个视图的最新内容(不进入交互模式)
|
|
819
|
+
if (subcommand === '__fzf-view') {
|
|
820
|
+
const key = args[1]
|
|
821
|
+
const content = key === 'skill' ? buildSkillViewInput() : buildSinglePlatformViewInput(key)
|
|
822
|
+
process.stdout.write(content + '\n')
|
|
823
|
+
process.exit(0)
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// 构建"按 skill 维度"的 fzf 输入行(skill/tool/hook 都在同一列表里)
|
|
827
|
+
function buildSkillViewInput() {
|
|
828
|
+
const skillItems = getAllSkillItems()
|
|
809
829
|
const toolItems = getAllToolItems()
|
|
810
830
|
const hookItems = getAllHookItems()
|
|
811
|
-
const previewPath = path.join(__dirname, 'preview.mjs')
|
|
812
831
|
|
|
813
|
-
//
|
|
832
|
+
// 每行 "NAME\tVERSION\tBUNDLE\tKIND\tSRCPATH\tINSTALLSCOPE"
|
|
814
833
|
const lines = [
|
|
815
834
|
...skillItems.map(s => {
|
|
816
835
|
const bundle = s.srcPath.split('/').slice(-2, -1)[0]
|
|
@@ -823,18 +842,6 @@ function fzfSelect() {
|
|
|
823
842
|
const nameWidth = Math.max(...lines.map(l => l.split('\t')[0].length))
|
|
824
843
|
const versionWidth = Math.max(...lines.map(l => l.split('\t')[1].length))
|
|
825
844
|
|
|
826
|
-
const G = '\x1b[32m', Y = '\x1b[33m', C = '\x1b[36m', D = '\x1b[2m', R = '\x1b[0m'
|
|
827
|
-
function colorIcon(status) {
|
|
828
|
-
if (status === 'up-to-date') return G + '✓' + R
|
|
829
|
-
if (status === 'update') return Y + '↑' + R
|
|
830
|
-
return D + '—' + R
|
|
831
|
-
}
|
|
832
|
-
function scopeHint(installScope) {
|
|
833
|
-
if (installScope === 'essential') return Y + '»' + R
|
|
834
|
-
if (installScope === 'global' || installScope === 'project') return C + '▸' + R
|
|
835
|
-
return D + '—' + R
|
|
836
|
-
}
|
|
837
|
-
|
|
838
845
|
// fzf 展示格式:NAME VERSION U:? P:? BUNDLE
|
|
839
846
|
const displayLines = lines.map(l => {
|
|
840
847
|
const [name, ver, bundle, kind, srcPath, installScope] = l.split('\t')
|
|
@@ -857,29 +864,106 @@ function fzfSelect() {
|
|
|
857
864
|
return `${name.padEnd(nameWidth)} ${ver.padEnd(versionWidth)} U:${uIcon} P:${pIcon} ${bundle}`
|
|
858
865
|
})
|
|
859
866
|
|
|
860
|
-
|
|
861
|
-
|
|
867
|
+
return displayLines.map((d, i) => `${d}\t${lines[i]}`).join('\n')
|
|
868
|
+
}
|
|
862
869
|
|
|
863
|
-
|
|
870
|
+
// 构建"单平台维度"的 fzf 输入行:每个 skill 一行,只展示该平台(user-scope)的安装状态
|
|
871
|
+
function buildSinglePlatformViewInput(targetName) {
|
|
872
|
+
const skillItems = getAllSkillItems()
|
|
873
|
+
const nameWidth = Math.max(...skillItems.map(s => s.skillName.length))
|
|
864
874
|
|
|
865
|
-
const
|
|
866
|
-
'
|
|
867
|
-
'
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
`--preview=node ${previewPath} {2} {3} {5} {6}`,
|
|
876
|
-
'--preview-window=right:42%:wrap',
|
|
877
|
-
], {
|
|
878
|
-
input: fzfInput,
|
|
879
|
-
encoding: 'utf8',
|
|
880
|
-
stdio: ['pipe', 'pipe', 'inherit'],
|
|
875
|
+
const lines = skillItems.map(s => {
|
|
876
|
+
const bundle = s.srcPath.split('/').slice(-2, -1)[0]
|
|
877
|
+
return `${s.skillName}\t${s.version ?? '—'}\t${bundle}\tskill\t${s.srcPath}\t${s.installScope ?? ''}`
|
|
878
|
+
})
|
|
879
|
+
|
|
880
|
+
const displayLines = skillItems.map(s => {
|
|
881
|
+
const installed = checkInstalled(s.skillName, s.version ?? '—')
|
|
882
|
+
const detail = installed.user[targetName]
|
|
883
|
+
const ver = detail.status === 'none' ? D + '—'.padEnd(7) + R : detail.version.padEnd(7)
|
|
884
|
+
return `${s.skillName.padEnd(nameWidth)} ${colorIcon(detail.status)} ${ver}`
|
|
881
885
|
})
|
|
882
886
|
|
|
887
|
+
return displayLines.map((d, i) => `${d}\t${lines[i]}`).join('\n')
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function platformPrompt(key) {
|
|
891
|
+
return ` ${key} › `
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// 静态 header:不随视图切换而变化,避免把 ANSI/中文塞进 fzf 的 shell 动作字符串里
|
|
895
|
+
const COMBINED_HEADER = ` hskill · ${G}✓${R}=ok ${Y}↑${R}=update ${Y}»${R}=essential ${C}▸${R}=recommended ${D}—${R}=none · ctrl-t 切平台 ctrl-u 一键升级本平台 ctrl-g 升级该 skill 全平台 · tab 多选 · enter 确认`
|
|
896
|
+
|
|
897
|
+
// 用 fzf 交互式选择 skill/tool,返回选中的 item 列表
|
|
898
|
+
function fzfSelect() {
|
|
899
|
+
requireFzf()
|
|
900
|
+
const previewPath = path.join(__dirname, 'preview.mjs')
|
|
901
|
+
const selfPath = process.argv[1]
|
|
902
|
+
// 用不带 flag 的 read(bash/zsh 通用;-n/-p/-k 等 flag 在两者语义不同,混用会挂住)等待回车再回到 fzf
|
|
903
|
+
const pauseCmd = `; printf '\\n 按 enter 返回…'; read _`
|
|
904
|
+
|
|
905
|
+
// 视图顺序:skill 维度 → 逐个平台维度(每次只显示一个平台)→ 循环回 skill
|
|
906
|
+
const viewKeys = ['skill', ...SKILL_TARGETS]
|
|
907
|
+
const skillViewInput = buildSkillViewInput()
|
|
908
|
+
const viewFiles = Object.fromEntries(viewKeys.map(key => [key, `/tmp/hskill-view-${key}-${process.pid}.txt`]))
|
|
909
|
+
|
|
910
|
+
for (const key of viewKeys) {
|
|
911
|
+
const content = key === 'skill' ? skillViewInput : buildSinglePlatformViewInput(key)
|
|
912
|
+
writeFileSync(viewFiles[key], content)
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// 每个视图对应的 preview 命令:单平台视图额外传入平台 key,让 preview.mjs 只输出该平台的状态
|
|
916
|
+
function previewCmdFor(key) {
|
|
917
|
+
const base = `node ${previewPath} {2} {3} {5} {6}`
|
|
918
|
+
return key === 'skill' ? base : `${base} ${key}`
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// ctrl-t 循环切换:根据当前 prompt 找到下一个视图,reload 对应文件、更新 prompt 和 preview 命令
|
|
922
|
+
const cycleCases = viewKeys.map((key, i) => {
|
|
923
|
+
const next = viewKeys[(i + 1) % viewKeys.length]
|
|
924
|
+
return `"${platformPrompt(key)}") echo "reload(cat ${viewFiles[next]})+change-prompt(${platformPrompt(next)})+change-preview(${previewCmdFor(next)})" ;;`
|
|
925
|
+
}).join('\n')
|
|
926
|
+
const cycleScript = `case "$FZF_PROMPT" in\n${cycleCases}\nesac`
|
|
927
|
+
|
|
928
|
+
// ctrl-u:仅在单平台视图下生效,一键升级该平台上所有已装 skill(user scope),完成后刷新当前视图
|
|
929
|
+
const upgradeAllCases = viewKeys.map(key => {
|
|
930
|
+
if (key === 'skill') return `"${platformPrompt(key)}") echo "" ;;`
|
|
931
|
+
const upgradeCmd = `node ${selfPath} upgrade --target ${key} --scope user${pauseCmd}`
|
|
932
|
+
return `"${platformPrompt(key)}") echo "execute(${upgradeCmd})+reload(node ${selfPath} __fzf-view ${key})" ;;`
|
|
933
|
+
}).join('\n')
|
|
934
|
+
const upgradeAllScript = `case "$FZF_PROMPT" in\n${upgradeAllCases}\nesac`
|
|
935
|
+
|
|
936
|
+
// ctrl-g:任意视图下,把光标所在的 skill 升级到它已安装的所有平台(user scope),完成后刷新当前视图
|
|
937
|
+
const upgradeSkillScript = `if [ {5} = skill ]; then key=$(echo "$FZF_PROMPT" | awk '{print $1}'); echo "execute(node ${selfPath} upgrade --skill {2}${pauseCmd})+reload(node ${selfPath} __fzf-view $key)"; else echo ""; fi`
|
|
938
|
+
|
|
939
|
+
let result
|
|
940
|
+
try {
|
|
941
|
+
result = spawnSync('fzf', [
|
|
942
|
+
'--multi',
|
|
943
|
+
'--ansi',
|
|
944
|
+
'--delimiter=\t',
|
|
945
|
+
'--with-nth=1',
|
|
946
|
+
`--prompt=${platformPrompt('skill')}`,
|
|
947
|
+
`--header=${COMBINED_HEADER}`,
|
|
948
|
+
'--layout=reverse',
|
|
949
|
+
'--border=rounded',
|
|
950
|
+
'--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
|
|
951
|
+
`--preview=node ${previewPath} {2} {3} {5} {6}`,
|
|
952
|
+
'--preview-window=right:42%:wrap',
|
|
953
|
+
`--bind=ctrl-t:transform:${cycleScript}`,
|
|
954
|
+
`--bind=ctrl-u:transform:${upgradeAllScript}`,
|
|
955
|
+
`--bind=ctrl-g:transform:${upgradeSkillScript}`,
|
|
956
|
+
], {
|
|
957
|
+
input: skillViewInput,
|
|
958
|
+
encoding: 'utf8',
|
|
959
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
960
|
+
})
|
|
961
|
+
} finally {
|
|
962
|
+
for (const file of Object.values(viewFiles)) {
|
|
963
|
+
try { unlinkSync(file) } catch { /* ignore */ }
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
883
967
|
if (result.status !== 0 || !result.stdout.trim()) return []
|
|
884
968
|
|
|
885
969
|
return result.stdout.trim().split('\n').map(line => {
|
package/bin/preview.mjs
CHANGED
|
@@ -11,6 +11,7 @@ const skillName = process.argv[2]?.trim()
|
|
|
11
11
|
const availableVersion = process.argv[3]?.trim() || '—'
|
|
12
12
|
const kind = process.argv[4]?.trim()
|
|
13
13
|
const srcPath = process.argv[5]?.trim()
|
|
14
|
+
const platformKey = process.argv[6]?.trim() || null
|
|
14
15
|
|
|
15
16
|
const G = '\x1b[32m', Y = '\x1b[33m', D = '\x1b[2m', R = '\x1b[0m', B = '\x1b[1m'
|
|
16
17
|
|
|
@@ -73,9 +74,7 @@ function statusLine(version, status) {
|
|
|
73
74
|
return ver + ' ' + D + '— not installed' + R
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
const
|
|
77
|
-
const projectTargets = SKILL_TARGETS.filter(t => !USER_ONLY_TARGETS.has(t))
|
|
78
|
-
const home = os.homedir()
|
|
77
|
+
const home = os.homedir()
|
|
79
78
|
|
|
80
79
|
function checkScope(targets, dirFn) {
|
|
81
80
|
return targets.map(t => {
|
|
@@ -87,6 +86,38 @@ function checkScope(targets, dirFn) {
|
|
|
87
86
|
})
|
|
88
87
|
}
|
|
89
88
|
|
|
89
|
+
// 单平台视图:只展示 ctrl-t 当前所在平台的状态,不展示全平台矩阵
|
|
90
|
+
if (platformKey && SKILL_TARGETS.includes(platformKey)) {
|
|
91
|
+
const cwd = process.cwd()
|
|
92
|
+
const userDir = path.join(home, `.${platformKey}`, 'skills')
|
|
93
|
+
const projectDir = path.join(cwd, `.${platformKey}`, 'skills')
|
|
94
|
+
const [userDetail] = checkScope([platformKey], () => userDir)
|
|
95
|
+
const projectDetail = USER_ONLY_TARGETS.has(platformKey) || cwd === home
|
|
96
|
+
? { version: '—', status: 'none' }
|
|
97
|
+
: checkScope([platformKey], () => projectDir)[0]
|
|
98
|
+
|
|
99
|
+
console.log(B + skillName + R)
|
|
100
|
+
console.log(D + 'available: ' + R + availableVersion)
|
|
101
|
+
console.log('')
|
|
102
|
+
|
|
103
|
+
console.log(B + platformKey.toUpperCase() + ' STATUS' + R)
|
|
104
|
+
console.log(' ' + 'user'.padEnd(8) + statusLine(userDetail.version, userDetail.status))
|
|
105
|
+
console.log(' ' + 'project'.padEnd(8) + statusLine(projectDetail.version, projectDetail.status))
|
|
106
|
+
console.log('')
|
|
107
|
+
|
|
108
|
+
if (userDetail.status === 'update' || projectDetail.status === 'update') {
|
|
109
|
+
console.log(Y + 'ACTION: update → ' + platformKey + R)
|
|
110
|
+
} else if (userDetail.status === 'none' && projectDetail.status === 'none') {
|
|
111
|
+
console.log(D + 'STATUS: not installed' + R)
|
|
112
|
+
} else {
|
|
113
|
+
console.log(G + 'STATUS: ok' + R)
|
|
114
|
+
}
|
|
115
|
+
process.exit(0)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const userTargets = SKILL_TARGETS
|
|
119
|
+
const projectTargets = SKILL_TARGETS.filter(t => !USER_ONLY_TARGETS.has(t))
|
|
120
|
+
|
|
90
121
|
const cwd = process.cwd()
|
|
91
122
|
const userDetails = checkScope(userTargets, t => path.join(home, `.${t}`, 'skills'))
|
|
92
123
|
const projectDetails = cwd === home
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harveyz-skill",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Skill manager for Claude Code, Cursor, and Codex",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"skills/research/learn-paper/",
|
|
25
25
|
"skills/research/extract-cognition/",
|
|
26
26
|
"skills/research/probe-session/",
|
|
27
|
+
"skills/research/pdf-math-translate/",
|
|
27
28
|
"skills/creative/capture-todo/",
|
|
28
29
|
"skills/creative/capture-insight/",
|
|
29
30
|
"skills/coding/init-workflow/",
|
|
@@ -31,6 +32,8 @@
|
|
|
31
32
|
"skills/coding/init-goal/",
|
|
32
33
|
"skills/coding/question-me/",
|
|
33
34
|
"skills/coding/capture-vocab/",
|
|
35
|
+
"skills/coding/rephrase/",
|
|
36
|
+
"skills/coding/explain-pm/",
|
|
34
37
|
"skills/writing/forge-doc/",
|
|
35
38
|
"skills/writing/draw-diagram/",
|
|
36
39
|
"skills/writing/manage-dir/",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explain-pm
|
|
3
|
+
description: "Restate a recent technical explanation from a product-manager's analytical lens — user experience, product philosophy, execution mechanics, and architectural health — rather than translating jargon, and flag product-perspective concerns like scope, over-engineering, priority, or drift from user need. Triggers: '/explain-pm', '/explain-pm <topic or file>', 'explain this like a PM', 'restate from a PM perspective'."
|
|
4
|
+
user_invocable: true
|
|
5
|
+
version: "1.1.1"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# explain-pm — PM 视角复述
|
|
9
|
+
|
|
10
|
+
把一段技术性表述从 PM 视角复述:换成 PM 关注的分析维度,再视情况指出值得从产品角度重新考虑的地方。
|
|
11
|
+
|
|
12
|
+
## 触发
|
|
13
|
+
|
|
14
|
+
仅手动调用:`/explain-pm` 或 `/explain-pm <主题或文件路径>`。不自动检测触发,不主动插话。
|
|
15
|
+
|
|
16
|
+
## PM 视角的关注维度
|
|
17
|
+
|
|
18
|
+
不是语言转译——PM 本身熟悉基础技术语言。要转换的是关注点:
|
|
19
|
+
|
|
20
|
+
- **用户体验**:这个决定最终会让用户感受到什么变化?
|
|
21
|
+
- **产品哲学**:是否符合产品一贯的取舍和原则?
|
|
22
|
+
- **执行机制与流程**:具体怎么落地——谁来做、分几步、影响什么协作节奏?
|
|
23
|
+
- **架构健康度(次要)**:是否有明显的技术债或长期维护风险,只做提醒,不展开实现细节本身。
|
|
24
|
+
|
|
25
|
+
## 执行
|
|
26
|
+
|
|
27
|
+
1. **取材料**:带参数用参数指定的主题或文件;无参数则取调用前的最后一条 assistant 消息。
|
|
28
|
+
2. **换视角**:对照上面的关注维度重新审视材料——不必在回复里逐条列出,只是分析时的参考。
|
|
29
|
+
3. **提出关注点(视情况)**:判断原表述从 PM 角度是否有值得重新审视的地方,比如是否偏离用户实际需求、范围是否合理、是否过度工程化、优先级是否搞错了。如果原表述本身已经很贴近产品目标,挑不出问题,就只做视角复述,不硬造关注点。关注点如果成立,放在回复最后,不要打断前面的视角复述。
|
|
30
|
+
4. **输出**:在对话中直接回复,简短(几句话量级)。默认不写文件;仅当用户明确要求存档时,才把这段评论写入用户指定的文件。
|
|
31
|
+
|
|
32
|
+
## 不做
|
|
33
|
+
|
|
34
|
+
多轮追问式澄清、自动检测技术语言并主动触发、默认生成或保存文档、固定的输出小标题模板。
|
|
@@ -60,14 +60,23 @@ rm -f /tmp/gwi-test-msg-${TIMESTAMP}.txt
|
|
|
60
60
|
|
|
61
61
|
取 `push_rules.block_force_push` 第一个分支名作为 `PROTECTED_PUSH_BRANCH`。
|
|
62
62
|
|
|
63
|
+
`pre-push/force-push` 块用 `git merge-base --is-ancestor` 判断非 fast-forward,因此必须用真实存在于仓库历史中的两个 commit 来构造"远端不是本地祖先"的场景,不能用假造的 SHA 字符串(假 SHA 在真实仓库里既不是祖先也不是非祖先,`git merge-base` 会因对象不存在报错,不代表 force push 语义)。用当前 `HEAD` 作为 remote_sha、`HEAD~1` 作为 local_sha:remote(较新)不是 local(较旧)的祖先,等价于历史被回退重写。仓库只有一个提交(无 `HEAD~1`)时跳过此场景。
|
|
64
|
+
|
|
63
65
|
```bash
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
if git rev-parse -q --verify HEAD~1 >/dev/null 2>&1; then
|
|
67
|
+
REMOTE_SHA=$(git rev-parse HEAD)
|
|
68
|
+
LOCAL_SHA=$(git rev-parse HEAD~1)
|
|
69
|
+
output=$(printf "refs/heads/%s %s refs/heads/%s %s\n" \
|
|
70
|
+
"$PROTECTED_PUSH_BRANCH" "$LOCAL_SHA" "$PROTECTED_PUSH_BRANCH" "$REMOTE_SHA" \
|
|
71
|
+
| sh .githooks/pre-push 2>&1)
|
|
72
|
+
exit_code=$?
|
|
73
|
+
SKIPPED=0
|
|
74
|
+
else
|
|
75
|
+
SKIPPED=1
|
|
76
|
+
fi
|
|
68
77
|
```
|
|
69
78
|
|
|
70
|
-
**期望:** `exit_code = 1
|
|
79
|
+
**期望:** 仓库提交数 ≥ 2 时 `exit_code = 1`;仅有一个提交时标记为 SKIP,不计入 PASS/FAIL。
|
|
71
80
|
|
|
72
81
|
---
|
|
73
82
|
|
|
@@ -114,7 +123,13 @@ pre-push force push 到 <branch>(应拒绝) PASS ✅
|
|
|
114
123
|
pre-push 非法 tag 推送(应拒绝) PASS ✅
|
|
115
124
|
post-checkout 不合规分支名(应有 ⚠️ 警告) PASS ✅
|
|
116
125
|
──────────────────────────────────────────────────────
|
|
117
|
-
✅ 全部通过(N/N
|
|
126
|
+
✅ 全部通过(N/N,SKIP 不计入分母)。继续 Step 7...
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
若 force push 场景因仓库只有一个提交被跳过,对应行显示:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
pre-push force push 到 <branch>(应拒绝) SKIP ⏭️(仓库提交数 < 2,无法构造历史)
|
|
118
133
|
```
|
|
119
134
|
|
|
120
135
|
若有失败:
|
|
@@ -93,9 +93,22 @@ SUBJECT=$(printf '%s\n' "$SUBJECT" | sed '/^#/d' | sed 's/^[[:space:]]*//')
|
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
**格式检查 MANAGED 块(`commit-msg/format`):**
|
|
96
|
+
块内首行固定豁免合并提交(`MERGE_HEAD` 存在即视为合并提交,直接放行,不做格式/长度校验):
|
|
97
|
+
```sh
|
|
98
|
+
# --- BEGIN MANAGED: commit-msg/format (hash:<hash>) ---
|
|
99
|
+
[ -f "$(git rev-parse --git-dir)/MERGE_HEAD" ] && exit 0
|
|
100
|
+
if ! printf '%s\n' "$SUBJECT" | grep -qE "<pattern>"; then
|
|
101
|
+
echo "❌ 提交信息不符合 Conventional Commits 规范"
|
|
102
|
+
...
|
|
103
|
+
exit 1
|
|
104
|
+
fi
|
|
105
|
+
# --- END MANAGED: commit-msg/format ---
|
|
106
|
+
```
|
|
96
107
|
- `format: conventional`:将 `types` 列表拼接为正则 `^(feat|fix|...)(\(.+\))?: .+`;`require_scope: false` 时 scope 部分为可选 `(\(.+\))?`
|
|
97
108
|
- `format: regex`:使用配置的 `pattern` 字段直接作为 grep 表达式
|
|
98
109
|
|
|
110
|
+
若不豁免合并提交,git 默认生成的 "Merge branch '...'" 信息永远无法通过 Conventional Commits 校验;而若强行要求合并提交也写成 conventional 格式,pre-commit 中 `merge_source_branch()` 依赖 git 默认 `MERGE_MSG` 文本解析来源分支,自定义信息会导致解析失败。豁免合并提交让两个 hook 同时成立。
|
|
111
|
+
|
|
99
112
|
**长度检查 MANAGED 块(`commit-msg/length`):**
|
|
100
113
|
```sh
|
|
101
114
|
# --- BEGIN MANAGED: commit-msg/length (hash:<hash>) ---
|
|
@@ -145,15 +158,23 @@ while IFS=' ' read -r local_ref local_sha remote_ref remote_sha; do
|
|
|
145
158
|
# --- BEGIN MANAGED: pre-push/force-push (hash:<hash>) ---
|
|
146
159
|
refs/heads/main|refs/heads/staging)
|
|
147
160
|
if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then
|
|
148
|
-
echo "❌
|
|
161
|
+
echo "❌ 禁止删除受保护分支:$remote_ref"
|
|
149
162
|
exit 1
|
|
150
163
|
fi
|
|
164
|
+
if [ "$remote_sha" != "0000000000000000000000000000000000000000" ]; then
|
|
165
|
+
if ! git merge-base --is-ancestor "$remote_sha" "$local_sha" 2>/dev/null; then
|
|
166
|
+
echo "❌ 禁止强制推送到受保护分支:$remote_ref"
|
|
167
|
+
exit 1
|
|
168
|
+
fi
|
|
169
|
+
fi
|
|
151
170
|
;;
|
|
152
171
|
# --- END MANAGED: pre-push/force-push ---
|
|
153
172
|
```
|
|
154
173
|
|
|
155
174
|
`refs/heads/main|refs/heads/staging` 从 `push_rules.block_force_push` 列表动态生成。
|
|
156
175
|
|
|
176
|
+
`local_sha` 全零表示删除远端分支,与 force push 是两码事,单独拦截并报出不同的提示。真正的非 fast-forward(force push)检测用 `git merge-base --is-ancestor "$remote_sha" "$local_sha"`:若远端当前 SHA 不是本地待推送 SHA 的祖先,说明历史被重写,即 force push;`remote_sha` 全零(推送全新分支)时跳过该判断,因为此时没有旧历史可比较。
|
|
177
|
+
|
|
157
178
|
**固定尾部:**
|
|
158
179
|
```sh
|
|
159
180
|
esac
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: question-me
|
|
3
3
|
description: "Pre-task clarification skill — clarifies ambiguous or complex tasks before execution through structured Q&A with a live decision tree. One question at a time, each with at least 3 options + custom, in decision-dependency order. Triggers: '/question-me', 'help me clarify this', 'question me before starting', 'let's define this first'. Claude auto-triggers when detecting ambiguous or complex requests (multiple conflicting goals, vague keywords like 'optimize/refactor/clean up', missing success criteria, unstated context assumptions)."
|
|
4
4
|
user_invocable: true
|
|
5
|
-
version: "3.0.
|
|
5
|
+
version: "3.0.1"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# question-me — 执行前指令澄清
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rephrase
|
|
3
|
+
description: "Single-pass rephrasing of a user statement into a more precise, less ambiguous, more actionable version, then either auto-proceeds or asks for confirmation based on Claude's own reliability judgment. Triggers: '/rephrase', '/rephrase <statement>', 'rephrase this', 'help me restate this more precisely'."
|
|
4
|
+
user_invocable: true
|
|
5
|
+
version: "1.0.1"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# rephrase — 单轮改写澄清
|
|
9
|
+
|
|
10
|
+
对用户的一句话表述做单轮改写,让它更精确、更少歧义、更可执行。不预设"合格表述"的标准或清单,由 Claude 依据当前语境自行判断。
|
|
11
|
+
|
|
12
|
+
## 触发
|
|
13
|
+
|
|
14
|
+
仅手动调用:`/rephrase` 或 `/rephrase <表述>`。不自动检测、不主动建议。多轮追问式澄清是 `question-me` 的职责,不是本 skill。
|
|
15
|
+
|
|
16
|
+
## 执行
|
|
17
|
+
|
|
18
|
+
1. **取待改写内容**:带参数用参数;无参数用用户上一条消息;两者都没有就直接问用户要表述。
|
|
19
|
+
2. **改写**:消歧义、补全隐含主语/宾语、明确动作对象,视原文缺什么而定——原文已经清楚就不用大改,也不要顺手加原文没提的验收标准/范围。
|
|
20
|
+
3. **判断可靠性**:不是看"改写里有没有任何未明说的假设"(几乎总有),而是看**猜错的代价**——如果涉及在多个同权重候选(哪个文件、哪个服务)里武断选一个,选错会做错事,判不可靠;如果只是无关紧要的实现细节、执行风险低、改错了也好回退,判可靠。
|
|
21
|
+
4. **分支**:可靠 → 展示改写结果,直接执行,不等确认。不可靠 → 展示改写结果和存疑点,等用户确认要不要改。
|
|
22
|
+
|
|
23
|
+
## 不做
|
|
24
|
+
|
|
25
|
+
多轮追问式澄清、强制补充验收标准/范围边界、自动触发。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: extract-url
|
|
3
|
-
version: "2.
|
|
3
|
+
version: "2.6.0"
|
|
4
4
|
description: "Use when a user provides a URL and wants to save, archive, fetch, or translate content to the local Obsidian Vault — even with vague phrasing like 'save this article', 'translate and save', 'put this in obsidian', 'archive this'. Skip when user only wants a summary, pastes raw text without a URL, asks about a site's tech stack, or wants to extract/list URLs from a page without saving an article."
|
|
5
5
|
user_invocable: true
|
|
6
6
|
---
|
|
@@ -73,30 +73,30 @@ ls ~/.hskill/url-extract/config.json 2>/dev/null && echo "EXISTS" || echo "NOT_F
|
|
|
73
73
|
## 路径变量(脚本自读 config.json,无需 Agent 传参)
|
|
74
74
|
|
|
75
75
|
```
|
|
76
|
-
Config:
|
|
77
|
-
Base:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
Config: ~/.hskill/url-extract/config.json
|
|
77
|
+
Base: VAULT_PATH (脚本从 config.json 读取)
|
|
78
|
+
ArticleDir: VAULT_PATH/<hash8> (hash8 = md5(source_url)[:8],由 scripts/config.py 的 get_article_paths() 统一计算)
|
|
79
|
+
Origin: ArticleDir/Origin
|
|
80
|
+
Translation: ArticleDir/Translation
|
|
81
|
+
Image: ArticleDir/Image
|
|
82
|
+
Meta: ArticleDir/meta.json
|
|
83
|
+
SkillDir: 平台固定值(见平台补丁)
|
|
82
84
|
```
|
|
83
85
|
|
|
84
86
|
---
|
|
85
87
|
|
|
86
|
-
## URL 去重索引(
|
|
88
|
+
## URL 去重索引(meta.json)
|
|
87
89
|
|
|
88
|
-
|
|
90
|
+
**索引路径:** `VAULT_PATH/<hash8>/meta.json`(`hash8` 由 URL 派生,去重时直接检查该路径是否存在,无需数据库)
|
|
89
91
|
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
article_path TEXT
|
|
99
|
-
);
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"source_url": "https://example.com/article",
|
|
95
|
+
"title": "文章标题",
|
|
96
|
+
"category": "分类",
|
|
97
|
+
"fetched_at": "2026-07-17",
|
|
98
|
+
"issues": ""
|
|
99
|
+
}
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
---
|
|
@@ -149,7 +149,7 @@ url_safe = re.sub(r'[\x00-\x1f\x7f]', '', url).strip()[:2048]
|
|
|
149
149
|
```
|
|
150
150
|
── 完成 ──────────────────────────────
|
|
151
151
|
标题 《文章标题》
|
|
152
|
-
路径 /Vault/Reading/article.md
|
|
152
|
+
路径 /Vault/Reading/a1b2c3d4/Translation/article.md
|
|
153
153
|
字符 12,345
|
|
154
154
|
代码 3 段
|
|
155
155
|
图片 8 张
|
|
@@ -169,7 +169,7 @@ url_safe = re.sub(r'[\x00-\x1f\x7f]', '', url).strip()[:2048]
|
|
|
169
169
|
```
|
|
170
170
|
── 部分完成 ───────────────────────────
|
|
171
171
|
标题 《文章标题》
|
|
172
|
-
路径 /Vault/Origin/article.md(仅原文)
|
|
172
|
+
路径 /Vault/Reading/a1b2c3d4/Origin/article.md(仅原文)
|
|
173
173
|
原因 翻译超时,原文已保存
|
|
174
174
|
──────────────────────────────────────
|
|
175
175
|
```
|
|
Binary file
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""
|
|
2
2
|
共享工具函数:格式化 block、构建文章、修复 frontmatter
|
|
3
3
|
"""
|
|
4
|
-
import re, os, json,
|
|
4
|
+
import re, os, json, yaml
|
|
5
5
|
from datetime import datetime, timezone, timedelta
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
# ------------------------------------------------------------
|
|
8
9
|
# 公共常量
|
|
@@ -113,7 +114,7 @@ description: {description}
|
|
|
113
114
|
# ------------------------------------------------------------
|
|
114
115
|
# 4. repair_frontmatter: 自动修复 frontmatter 字段
|
|
115
116
|
# ------------------------------------------------------------
|
|
116
|
-
def repair_frontmatter(fp, url, defaults=None):
|
|
117
|
+
def repair_frontmatter(fp, url, defaults=None, skip_remaining_fields=None):
|
|
117
118
|
"""
|
|
118
119
|
尝试自动补全缺失字段,返回 (fm_dict, 修复了哪些字段, 剩余问题列表)
|
|
119
120
|
"""
|
|
@@ -236,35 +237,35 @@ def repair_frontmatter(fp, url, defaults=None):
|
|
|
236
237
|
f.write(fm_str + content[m.end():])
|
|
237
238
|
|
|
238
239
|
# 检查剩余问题
|
|
240
|
+
skip = set(skip_remaining_fields or ())
|
|
239
241
|
for fld in ['publish_date', 'author', 'source_url']:
|
|
240
|
-
if not fm.get(fld, '').strip():
|
|
242
|
+
if fld not in skip and not fm.get(fld, '').strip():
|
|
241
243
|
remaining.append(f'{fld}空')
|
|
242
|
-
if 'origin_title' not in fm or not fm.get('origin_title', '').strip():
|
|
244
|
+
if 'origin_title' not in skip and ('origin_title' not in fm or not fm.get('origin_title', '').strip()):
|
|
243
245
|
remaining.append('origin_title空')
|
|
244
|
-
if 'description' not in fm or not fm.get('description', '').strip():
|
|
246
|
+
if 'description' not in skip and ('description' not in fm or not fm.get('description', '').strip()):
|
|
245
247
|
remaining.append('description空')
|
|
246
248
|
|
|
247
249
|
return fm, fixed, remaining
|
|
248
250
|
|
|
249
251
|
|
|
250
252
|
# ------------------------------------------------------------
|
|
251
|
-
# 5.
|
|
253
|
+
# 5. record_fetch_issues: Subagent 1 阶段的问题写入临时文件,供 write_meta_json 合并
|
|
252
254
|
# ------------------------------------------------------------
|
|
253
|
-
def
|
|
254
|
-
"""将
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
conn.commit()
|
|
255
|
+
def record_fetch_issues(issues_text, article_dir):
|
|
256
|
+
"""将 Subagent 1 阶段校验问题写入 <article_dir>/.fetch_issues.tmp,供 write_meta_json 合并。"""
|
|
257
|
+
tmp_path = Path(article_dir) / '.fetch_issues.tmp'
|
|
258
|
+
if issues_text:
|
|
259
|
+
tmp_path.write_text(issues_text, encoding='utf-8')
|
|
260
|
+
elif tmp_path.exists():
|
|
261
|
+
tmp_path.unlink()
|
|
261
262
|
|
|
262
263
|
|
|
263
264
|
# ------------------------------------------------------------
|
|
264
|
-
# 6.
|
|
265
|
+
# 6. write_meta_json: 写入 <hash8>/meta.json
|
|
265
266
|
# ------------------------------------------------------------
|
|
266
|
-
def
|
|
267
|
-
"""
|
|
267
|
+
def write_meta_json(url, meta_path, article_path, category=''):
|
|
268
|
+
"""Write (or overwrite) meta.json after successful fetch+translate, merging any pending fetch-stage issues."""
|
|
268
269
|
fetch_date = datetime.now(timezone(timedelta(hours=8))).strftime('%Y-%m-%d')
|
|
269
270
|
fm = {}
|
|
270
271
|
try:
|
|
@@ -277,14 +278,23 @@ def write_url_index(url, origin_path, article_path, db_path, category=''):
|
|
|
277
278
|
except Exception:
|
|
278
279
|
pass
|
|
279
280
|
cat = category or (fm.get('category') or '')
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
281
|
+
|
|
282
|
+
meta_path = Path(meta_path)
|
|
283
|
+
meta_path.parent.mkdir(parents=True, exist_ok=True)
|
|
284
|
+
tmp_issues_path = meta_path.parent / '.fetch_issues.tmp'
|
|
285
|
+
issues = ''
|
|
286
|
+
if tmp_issues_path.exists():
|
|
287
|
+
issues = tmp_issues_path.read_text(encoding='utf-8').strip()
|
|
288
|
+
tmp_issues_path.unlink()
|
|
289
|
+
|
|
290
|
+
meta = {
|
|
291
|
+
'source_url': url,
|
|
292
|
+
'title': os.path.basename(article_path),
|
|
293
|
+
'category': cat,
|
|
294
|
+
'fetched_at': fetch_date,
|
|
295
|
+
'issues': issues,
|
|
296
|
+
}
|
|
297
|
+
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
288
298
|
|
|
289
299
|
|
|
290
300
|
# ------------------------------------------------------------
|