harveyz-skill 0.33.0 → 0.34.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 +11 -0
- package/bin/cli.js +170 -27
- package/lib/install-source.js +46 -0
- package/lib/version-check.js +2 -2
- package/package.json +7 -1
- package/skills/agent-canvas/agent-canvas-control/SKILL.md +235 -0
- package/skills/agent-canvas/capture-requirement/SKILL.md +51 -0
- package/skills/agent-canvas/close-node/SKILL.md +117 -0
- package/skills/agent-canvas/describe-node/SKILL.md +47 -0
- package/skills/agent-canvas/relate-node/SKILL.md +63 -0
- package/skills/agent-canvas/relation-review/SKILL.md +120 -0
- package/skills-index.json +26 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.34.0] - 2026-09-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `hskill` 双来源安装与粘性更新:`hskill update --local <路径>` 从本地仓库 `npm pack` 出 tarball 再全局安装(真实拷贝,走 `prepack` 与 `files[]` 白名单,等于在发 npm 前预演一次真实发布),`hskill update --npm` 切回 registry,裸 `hskill update` 沿用当前来源不跨轨道。此前 `update` 是一行硬编码的 `npm install -g harveyz-skill@latest`,手工装的本地版会被它静默替换掉且事后无从察觉。来源痕迹(`.hskill-source.json` + 版本号后缀 `+local`)只写在全局安装目录内,因而任何一次 `npm install -g`——包括绕过 hskill 的手工安装——都会自动抹掉它,"记录的来源"与"实际的来源"不可能分叉。`hskill version` 展示来源与 branch/commit,`version --check` 在本地来源下比对 commit 而非版本号(开发分支上版本号常常不动)。设计见 `docs/superpowers/specs/2026-09-15-hskill-install-source-design.md`
|
|
14
|
+
- `agent-canvas` bundle:新增 `agent-canvas-control`、`capture-requirement`、`close-node`、`describe-node`、`relate-node`、`relation-review` 六个 skill,在 Agent Canvas 画布节点里操控节点实体、摆放画布、梳理节点间语义关系
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- `hskill <任意子命令> --json` 在输出走管道时被截断:Node 写管道是异步的,而每个子命令块以 `process.exit()` 结尾,不等缓冲区排空。`status --json` 的输出刚随 agent-canvas 六个 skill 涨到 66,739 字节、越过 64KiB 管道缓冲区,于是 `hskill status --json | jq` 拿到退出码 0 和一段从字符串中间断掉的 JSON——**静默失败**,退出码还宣称成功。16 处 `--json` 出口统一改走等待写入落地的 `emitJson()`;回归测试逐字节比对管道输出与直接重定向输出,且在负载跌回缓冲区以下时显式 skip 而非假绿
|
|
18
|
+
- `hskill version` / `hskill --version` 在 npm 不在 PATH 上时抛原始堆栈崩溃(cron、受限 PATH、精简镜像):来源追踪让 `readSource()` 每次都经 `globalRoot()` 打一次 `npm root -g`,而它只兜住了 JSON 解析失败。现在 `globalRoot()` 的失败并入同一个 `null`,即回落到缺省的 npm 来源
|
|
19
|
+
- `compareVersions()` 遇到 semver build metadata(`0.33.0+local`)会解析出 `NaN` 导致比较结果无意义,现按规范在比较前截断 `+` 及其后内容
|
|
20
|
+
|
|
10
21
|
## [0.33.0] - 2026-09-15
|
|
11
22
|
|
|
12
23
|
### Added
|
package/bin/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { select, input, confirm } from '@inquirer/prompts'
|
|
3
3
|
import chalk from 'chalk'
|
|
4
4
|
import { execSync, spawnSync } from 'child_process'
|
|
5
|
-
import { existsSync, writeFileSync, unlinkSync } from 'fs'
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdtempSync } from 'fs'
|
|
6
6
|
import { createRequire } from 'module'
|
|
7
7
|
import os from 'os'
|
|
8
8
|
import path from 'path'
|
|
@@ -24,6 +24,16 @@ const args = process.argv.slice(2)
|
|
|
24
24
|
const subcommand = args[0]
|
|
25
25
|
const jsonFlag = args.includes('--json')
|
|
26
26
|
|
|
27
|
+
// Writing to a pipe is asynchronous in Node, and process.exit() throws away
|
|
28
|
+
// whatever is still buffered. Anything past the 64KiB pipe buffer was being cut
|
|
29
|
+
// mid-string, so `hskill status --json | jq` got exit code 0 and unparseable
|
|
30
|
+
// JSON. Every --json emitter goes through here, which resolves only once the
|
|
31
|
+
// write has actually landed.
|
|
32
|
+
async function emitJson(value) {
|
|
33
|
+
const text = JSON.stringify(value, null, 2) + '\n'
|
|
34
|
+
await new Promise(resolve => process.stdout.write(text, resolve))
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
// ── Help ─────────────────────────────────────────────────────────────────────
|
|
28
38
|
function printHelp() {
|
|
29
39
|
console.log(`
|
|
@@ -50,10 +60,12 @@ function printHelp() {
|
|
|
50
60
|
hskill uninstall <tool> uninstall a shell tool and clean up all files
|
|
51
61
|
hskill uninstall <tool> --yes skip all confirmations (incl. config files)
|
|
52
62
|
hskill uninstall <skill> --scope <s> --target <t> uninstall a skill
|
|
53
|
-
hskill update update hskill
|
|
63
|
+
hskill update update hskill (sticky: refreshes from whichever source is installed)
|
|
64
|
+
hskill update --local <path> switch to / refresh a local repo (npm pack + install)
|
|
65
|
+
hskill update --npm switch back to npm registry
|
|
54
66
|
hskill mcp start an MCP server (stdio) exposing hskill's tools to MCP-capable agent hosts
|
|
55
|
-
hskill version show version
|
|
56
|
-
hskill version --check compare
|
|
67
|
+
hskill version show version (adds source/branch/commit lines when installed from a local repo)
|
|
68
|
+
hskill version --check compare against npm registry, or against the local source repo's HEAD
|
|
57
69
|
hskill --help show this help
|
|
58
70
|
|
|
59
71
|
${chalk.cyan('Examples:')}
|
|
@@ -74,7 +86,7 @@ function printHelp() {
|
|
|
74
86
|
|
|
75
87
|
if (args[0] === '--help' || args[0] === '-h') {
|
|
76
88
|
if (jsonFlag || args.includes('--json')) {
|
|
77
|
-
|
|
89
|
+
await emitJson({
|
|
78
90
|
name: 'hskill',
|
|
79
91
|
version,
|
|
80
92
|
description: 'Skill manager for Claude Code, Cursor, Codex, OpenClaw, Hermes, OpenCode, and Pi',
|
|
@@ -138,7 +150,11 @@ if (args[0] === '--help' || args[0] === '-h') {
|
|
|
138
150
|
},
|
|
139
151
|
{
|
|
140
152
|
name: 'update',
|
|
141
|
-
description: 'Update hskill to
|
|
153
|
+
description: 'Update hskill; sticky to whichever source (npm or local repo) is currently installed',
|
|
154
|
+
flags: [
|
|
155
|
+
{ name: '--local', arg: '<path>', description: 'Switch to / refresh a local repo source (npm pack + install -g)' },
|
|
156
|
+
{ name: '--npm', description: 'Switch back to the npm registry source' },
|
|
157
|
+
],
|
|
142
158
|
},
|
|
143
159
|
{
|
|
144
160
|
name: 'mcp',
|
|
@@ -149,7 +165,7 @@ if (args[0] === '--help' || args[0] === '-h') {
|
|
|
149
165
|
description: 'Print version and exit',
|
|
150
166
|
},
|
|
151
167
|
],
|
|
152
|
-
}
|
|
168
|
+
})
|
|
153
169
|
process.exit(0)
|
|
154
170
|
}
|
|
155
171
|
printHelp()
|
|
@@ -157,12 +173,37 @@ if (args[0] === '--help' || args[0] === '-h') {
|
|
|
157
173
|
}
|
|
158
174
|
|
|
159
175
|
if (args[0] === '--version' || args[0] === '-v' || subcommand === 'version') {
|
|
176
|
+
const { readSource, gitInfo } = await import('../lib/install-source.js')
|
|
177
|
+
const source = readSource()
|
|
178
|
+
|
|
160
179
|
if (subcommand === 'version' && args.includes('--check')) {
|
|
180
|
+
if (source) {
|
|
181
|
+
let current
|
|
182
|
+
try {
|
|
183
|
+
current = gitInfo(source.repo)
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.error(chalk.red(` ✗ Could not read local source repo: ${err.message}`))
|
|
186
|
+
process.exit(1)
|
|
187
|
+
}
|
|
188
|
+
const upToDate = current.commit === source.commit && !current.dirty
|
|
189
|
+
if (jsonFlag) {
|
|
190
|
+
await emitJson({ source: 'local', repo: source.repo, installedCommit: source.commit, currentCommit: current.commit, dirty: current.dirty, upToDate })
|
|
191
|
+
} else if (upToDate) {
|
|
192
|
+
console.log(chalk.green(` ✔ hskill is up to date with local source (${source.repo}@${current.commit})`))
|
|
193
|
+
} else if (current.commit !== source.commit) {
|
|
194
|
+
console.log(chalk.yellow(` ⚠ local source has new commits: ${source.commit} → ${current.commit}`))
|
|
195
|
+
console.log(chalk.dim(' Run: hskill update'))
|
|
196
|
+
} else {
|
|
197
|
+
console.log(chalk.yellow(' ⚠ local source has uncommitted changes not yet packed'))
|
|
198
|
+
console.log(chalk.dim(' Run: hskill update'))
|
|
199
|
+
}
|
|
200
|
+
process.exit(0)
|
|
201
|
+
}
|
|
161
202
|
try {
|
|
162
203
|
const { checkNpmVersion } = await import('../lib/version-check.js')
|
|
163
204
|
const { current, latest, upToDate } = await checkNpmVersion('harveyz-skill', version)
|
|
164
205
|
if (jsonFlag) {
|
|
165
|
-
|
|
206
|
+
await emitJson({ current, latest, upToDate })
|
|
166
207
|
} else if (upToDate) {
|
|
167
208
|
console.log(chalk.green(` ✔ hskill v${current} is up to date`))
|
|
168
209
|
} else {
|
|
@@ -176,6 +217,11 @@ if (args[0] === '--version' || args[0] === '-v' || subcommand === 'version') {
|
|
|
176
217
|
process.exit(0)
|
|
177
218
|
}
|
|
178
219
|
console.log(version)
|
|
220
|
+
if (source) {
|
|
221
|
+
console.log('')
|
|
222
|
+
console.log(`source: local ${source.repo}`)
|
|
223
|
+
console.log(`branch: ${source.branch} commit: ${source.commit}${source.dirty ? ' (dirty)' : ''}`)
|
|
224
|
+
}
|
|
179
225
|
process.exit(0)
|
|
180
226
|
}
|
|
181
227
|
|
|
@@ -223,15 +269,112 @@ async function checkArchivedInstalls() {
|
|
|
223
269
|
}
|
|
224
270
|
|
|
225
271
|
// ── Update ───────────────────────────────────────────────────────────────────
|
|
226
|
-
|
|
272
|
+
// Sticky: whichever source is currently installed (recorded in .hskill-source.json
|
|
273
|
+
// inside the global install dir) is what a bare `update` refreshes from. `--local`
|
|
274
|
+
// and `--npm` explicitly switch the source. See docs/superpowers/specs/2026-09-15-hskill-install-source-design.md
|
|
275
|
+
async function updateToNpm(priorSource) {
|
|
227
276
|
console.log(chalk.dim(' · Updating hskill…'))
|
|
228
277
|
try {
|
|
229
278
|
execSync('npm install -g harveyz-skill@latest', { stdio: 'inherit' })
|
|
230
|
-
console.log(chalk.green(' ✔ hskill updated'))
|
|
231
279
|
} catch {
|
|
232
280
|
console.error(chalk.red(' ✗ Update failed. Try: npm install -g harveyz-skill@latest'))
|
|
233
281
|
process.exit(1)
|
|
234
282
|
}
|
|
283
|
+
if (priorSource) {
|
|
284
|
+
const { globalRoot } = await import('../lib/install-source.js')
|
|
285
|
+
const newVersion = JSON.parse(readFileSync(path.join(globalRoot(), 'harveyz-skill', 'package.json'), 'utf8')).version
|
|
286
|
+
console.log(` ${priorSource.version}+local (${priorSource.branch}@${priorSource.commit}) → ${newVersion} (npm)`)
|
|
287
|
+
}
|
|
288
|
+
console.log(chalk.green(' ✔ hskill updated'))
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function updateToLocal(repoPath, priorSource) {
|
|
292
|
+
if (!existsSync(repoPath)) {
|
|
293
|
+
console.error(chalk.red(` ✗ 本地来源仓库不存在:${repoPath}`))
|
|
294
|
+
console.error(chalk.dim(' 改用 npm: hskill update --npm'))
|
|
295
|
+
console.error(chalk.dim(` 指向新路径: hskill update --local <新路径>`))
|
|
296
|
+
process.exit(1)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const { gitInfo, writeSource, globalRoot } = await import('../lib/install-source.js')
|
|
300
|
+
// git info must be collected before `npm pack` — its `prepack` hook rewrites
|
|
301
|
+
// the tracked .npmignore, which would otherwise poison the dirty check.
|
|
302
|
+
const info = gitInfo(repoPath)
|
|
303
|
+
const origVersion = JSON.parse(readFileSync(path.join(repoPath, 'package.json'), 'utf8')).version
|
|
304
|
+
|
|
305
|
+
console.log(chalk.dim(' · Packing local repo…'))
|
|
306
|
+
const tmpDir = mkdtempSync(path.join(os.tmpdir(), 'hskill-pack-'))
|
|
307
|
+
const packResult = spawnSync('npm', ['pack', '--pack-destination', tmpDir], { cwd: repoPath, encoding: 'utf8' })
|
|
308
|
+
if (packResult.status !== 0) {
|
|
309
|
+
console.error(chalk.red(' ✗ npm pack failed'))
|
|
310
|
+
process.exit(1)
|
|
311
|
+
}
|
|
312
|
+
const tarballName = packResult.stdout.trim().split('\n').pop()
|
|
313
|
+
|
|
314
|
+
console.log(chalk.dim(' · Installing packed tarball…'))
|
|
315
|
+
const installResult = spawnSync('npm', ['install', '-g', path.join(tmpDir, tarballName)], { stdio: 'inherit' })
|
|
316
|
+
if (installResult.status !== 0) {
|
|
317
|
+
console.error(chalk.red(' ✗ Install failed'))
|
|
318
|
+
process.exit(1)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const installedPkgPath = path.join(globalRoot(), 'harveyz-skill', 'package.json')
|
|
322
|
+
const installedPkg = JSON.parse(readFileSync(installedPkgPath, 'utf8'))
|
|
323
|
+
installedPkg.version = `${origVersion}+local`
|
|
324
|
+
writeFileSync(installedPkgPath, JSON.stringify(installedPkg, null, 2) + '\n')
|
|
325
|
+
|
|
326
|
+
writeSource({
|
|
327
|
+
repo: repoPath,
|
|
328
|
+
branch: info.branch,
|
|
329
|
+
commit: info.commit,
|
|
330
|
+
dirty: info.dirty,
|
|
331
|
+
version: origVersion,
|
|
332
|
+
installedAt: new Date().toISOString(),
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
if (priorSource === null) {
|
|
336
|
+
console.log(` ${version} (npm) → ${origVersion}+local (${info.branch}@${info.commit})`)
|
|
337
|
+
} else if (priorSource.repo !== repoPath) {
|
|
338
|
+
console.log(` ${priorSource.version}+local (${priorSource.branch}@${priorSource.commit}) → ${origVersion}+local (${info.branch}@${info.commit})`)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const npmignoreStatus = execSync('git status --porcelain -- .npmignore', { cwd: repoPath, encoding: 'utf8' }).trim()
|
|
342
|
+
if (npmignoreStatus) {
|
|
343
|
+
console.log(chalk.dim(' · .npmignore changed by prepack — left as-is, review with `git diff .npmignore`'))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
console.log(chalk.green(' ✔ hskill updated'))
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (subcommand === 'update') {
|
|
350
|
+
const updateArgs = args.slice(1)
|
|
351
|
+
const localIdx = updateArgs.indexOf('--local')
|
|
352
|
+
const npmFlag = updateArgs.includes('--npm')
|
|
353
|
+
const localGiven = localIdx !== -1
|
|
354
|
+
const localPath = localGiven ? updateArgs[localIdx + 1] : undefined
|
|
355
|
+
|
|
356
|
+
if (localGiven && npmFlag) {
|
|
357
|
+
console.error(chalk.red(' ✗ --local and --npm are mutually exclusive'))
|
|
358
|
+
process.exit(1)
|
|
359
|
+
}
|
|
360
|
+
if (localGiven && (!localPath || localPath.startsWith('--'))) {
|
|
361
|
+
console.error(chalk.red(' ✗ --local requires a path: hskill update --local <path>'))
|
|
362
|
+
process.exit(1)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const { readSource } = await import('../lib/install-source.js')
|
|
366
|
+
const priorSource = readSource()
|
|
367
|
+
|
|
368
|
+
if (npmFlag) {
|
|
369
|
+
await updateToNpm(priorSource)
|
|
370
|
+
} else if (localGiven) {
|
|
371
|
+
await updateToLocal(localPath, priorSource)
|
|
372
|
+
} else if (priorSource) {
|
|
373
|
+
await updateToLocal(priorSource.repo, priorSource)
|
|
374
|
+
} else {
|
|
375
|
+
await updateToNpm(priorSource)
|
|
376
|
+
}
|
|
377
|
+
|
|
235
378
|
// Run skill rename migrations
|
|
236
379
|
const { renames = [], skills: skillDefs = [] } = require('../skills-index.json')
|
|
237
380
|
if (renames.length > 0) {
|
|
@@ -276,10 +419,10 @@ if (subcommand === 'list') {
|
|
|
276
419
|
const { skills, tools = [] } = require('../skills-index.json')
|
|
277
420
|
const sorted = [...skills].sort((a, b) => a.bundle.localeCompare(b.bundle) || a.path.split('/').pop().localeCompare(b.path.split('/').pop()))
|
|
278
421
|
if (jsonFlag) {
|
|
279
|
-
|
|
422
|
+
await emitJson({
|
|
280
423
|
skills: sorted.map(s => ({ name: s.path.split('/').pop(), path: s.path, bundle: s.bundle, global: s.global ?? false })),
|
|
281
424
|
tools: tools.map(t => t.name),
|
|
282
|
-
}
|
|
425
|
+
})
|
|
283
426
|
process.exit(0)
|
|
284
427
|
}
|
|
285
428
|
const nw = Math.max(...sorted.map(s => s.path.split('/').pop().length), 4)
|
|
@@ -380,12 +523,12 @@ if (subcommand === 'status' || subcommand === 'outdated') {
|
|
|
380
523
|
return { name: h.name, description: h.description, user: inst.user, project: inst.project }
|
|
381
524
|
})
|
|
382
525
|
if (outdatedOnly) {
|
|
383
|
-
|
|
526
|
+
await emitJson({
|
|
384
527
|
skills: jsonSkills.filter(s => Object.values(s.user).some(v => v.status === 'update') || Object.values(s.project).some(v => v.status === 'update')),
|
|
385
528
|
tools: jsonTools.filter(t => t.status === 'update'),
|
|
386
|
-
}
|
|
529
|
+
})
|
|
387
530
|
} else {
|
|
388
|
-
|
|
531
|
+
await emitJson({ skills: jsonSkills, tools: jsonTools, hooks: jsonHooks })
|
|
389
532
|
}
|
|
390
533
|
process.exit(0)
|
|
391
534
|
}
|
|
@@ -513,17 +656,17 @@ if (subcommand === 'info') {
|
|
|
513
656
|
if (jsonFlag) {
|
|
514
657
|
if (skill) {
|
|
515
658
|
const inst = checkInstalled(skill.skillName, skill.version ?? '—')
|
|
516
|
-
|
|
659
|
+
await emitJson({
|
|
517
660
|
name: skill.skillName, type: 'skill', version: skill.version ?? '—',
|
|
518
661
|
user: Object.fromEntries(targets.map(t => [t, inst.user[t]])),
|
|
519
662
|
project: Object.fromEntries(targets.map(t => [t, inst.project[t]])),
|
|
520
|
-
}
|
|
663
|
+
})
|
|
521
664
|
} else {
|
|
522
665
|
const inst = checkToolInstalled(tool.toolName, tool.srcPath)
|
|
523
|
-
|
|
666
|
+
await emitJson({
|
|
524
667
|
name: tool.toolName, type: 'tool', version: tool.version ?? '—',
|
|
525
668
|
installed: inst,
|
|
526
|
-
}
|
|
669
|
+
})
|
|
527
670
|
}
|
|
528
671
|
process.exit(0)
|
|
529
672
|
}
|
|
@@ -590,7 +733,7 @@ if (subcommand === 'uninstall') {
|
|
|
590
733
|
const { removed, failed } = await uninstallTool(nameToRemove, { yes: yesFlag })
|
|
591
734
|
if (jsonFlag) {
|
|
592
735
|
console.error = originalError
|
|
593
|
-
|
|
736
|
+
await emitJson({ removed: removed.length > 0, failed: failed.length > 0 })
|
|
594
737
|
} else if (removed.length > 0) {
|
|
595
738
|
console.error(chalk.green.bold(`✔ ${nameToRemove} uninstalled`))
|
|
596
739
|
}
|
|
@@ -617,7 +760,7 @@ if (subcommand === 'uninstall') {
|
|
|
617
760
|
}
|
|
618
761
|
if (jsonFlag) {
|
|
619
762
|
console.error = originalError2
|
|
620
|
-
|
|
763
|
+
await emitJson({ removed: anyRemoved, failed: anyFailed })
|
|
621
764
|
} else if (anyRemoved) {
|
|
622
765
|
console.error(chalk.green.bold(`✔ ${nameToRemove} uninstalled`))
|
|
623
766
|
}
|
|
@@ -663,7 +806,7 @@ if (subcommand === 'hooks') {
|
|
|
663
806
|
codex: inst.codex,
|
|
664
807
|
}
|
|
665
808
|
})
|
|
666
|
-
|
|
809
|
+
await emitJson({ hooks: out })
|
|
667
810
|
process.exit(0)
|
|
668
811
|
}
|
|
669
812
|
function hookIcon(s) {
|
|
@@ -724,7 +867,7 @@ if (subcommand === 'hooks') {
|
|
|
724
867
|
const { installed, skipped, failed } = await installHooksForTarget(toInstall, hookTargetArg, hookScopeArg, hookProjectArg, hookForce)
|
|
725
868
|
|
|
726
869
|
if (hookJsonFlag) {
|
|
727
|
-
|
|
870
|
+
await emitJson({ installed, skipped, failed })
|
|
728
871
|
process.exit(failed.length ? 1 : 0)
|
|
729
872
|
} else {
|
|
730
873
|
if (installed.length) console.error(chalk.green.bold(`✔ Hooks installed (${hookScopeArg}):`), installed.join(', '))
|
|
@@ -746,7 +889,7 @@ if (subcommand === 'hooks') {
|
|
|
746
889
|
}
|
|
747
890
|
const { removed } = await uninstallHook(nameToRemove, hookScopeArg, hookProjectArg)
|
|
748
891
|
if (hookJsonFlag) {
|
|
749
|
-
|
|
892
|
+
await emitJson({ removed })
|
|
750
893
|
} else if (!removed) {
|
|
751
894
|
console.log(chalk.dim(` · ${nameToRemove} was not installed in ${hookScopeArg} scope`))
|
|
752
895
|
}
|
|
@@ -798,9 +941,9 @@ if (subcommand === 'upgrade') {
|
|
|
798
941
|
const nothingUpgraded = Object.keys(summary).length === 0
|
|
799
942
|
if (jsonFlag) {
|
|
800
943
|
if (nothingUpgraded) {
|
|
801
|
-
|
|
944
|
+
await emitJson({ skills: {}, upToDate: true })
|
|
802
945
|
} else {
|
|
803
|
-
|
|
946
|
+
await emitJson({ skills: summary })
|
|
804
947
|
}
|
|
805
948
|
} else {
|
|
806
949
|
if (nothingUpgraded) {
|
|
@@ -1512,7 +1655,7 @@ try {
|
|
|
1512
1655
|
const out = {}
|
|
1513
1656
|
if (skillSummary !== null) out.skills = skillSummary
|
|
1514
1657
|
if (toolSummary !== null) out.tools = toolSummary
|
|
1515
|
-
|
|
1658
|
+
await emitJson(out)
|
|
1516
1659
|
} else {
|
|
1517
1660
|
printSummary(skillSummary, toolSummary)
|
|
1518
1661
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { execSync } from 'child_process'
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
let cachedGlobalRoot = null
|
|
6
|
+
|
|
7
|
+
export function globalRoot() {
|
|
8
|
+
if (process.env.HSKILL_GLOBAL_ROOT) return process.env.HSKILL_GLOBAL_ROOT
|
|
9
|
+
// stdio 'pipe' keeps npm's own stderr off ours: callers that tolerate failure
|
|
10
|
+
// (readSource) would otherwise leak "npm: command not found" into clean output.
|
|
11
|
+
if (!cachedGlobalRoot) cachedGlobalRoot = execSync('npm root -g', { encoding: 'utf8', stdio: 'pipe' }).trim()
|
|
12
|
+
return cachedGlobalRoot
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function sourceFilePath() {
|
|
16
|
+
return path.join(globalRoot(), 'harveyz-skill', '.hskill-source.json')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// null means "npm source" — the default. An unreadable trace file and an
|
|
20
|
+
// unreachable npm both collapse into it: `version` must keep working where npm
|
|
21
|
+
// is off PATH, and a source we cannot read is not a source we can update from.
|
|
22
|
+
export function readSource() {
|
|
23
|
+
let file
|
|
24
|
+
try {
|
|
25
|
+
file = sourceFilePath()
|
|
26
|
+
} catch {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
if (!existsSync(file)) return null
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync(file, 'utf8'))
|
|
32
|
+
} catch {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function writeSource(info) {
|
|
38
|
+
writeFileSync(sourceFilePath(), JSON.stringify(info, null, 2) + '\n')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function gitInfo(repo) {
|
|
42
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: repo, encoding: 'utf8' }).trim()
|
|
43
|
+
const commit = execSync('git rev-parse --short HEAD', { cwd: repo, encoding: 'utf8' }).trim()
|
|
44
|
+
const dirty = execSync('git status --porcelain', { cwd: repo, encoding: 'utf8' }).trim().length > 0
|
|
45
|
+
return { branch, commit, dirty }
|
|
46
|
+
}
|
package/lib/version-check.js
CHANGED
|
@@ -2,8 +2,8 @@ const DEFAULT_REGISTRY = 'https://registry.npmjs.org'
|
|
|
2
2
|
const REGISTRY_TIMEOUT_MS = 5000
|
|
3
3
|
|
|
4
4
|
export function compareVersions(a, b) {
|
|
5
|
-
const pa = a.split('.').map(Number)
|
|
6
|
-
const pb = b.split('.').map(Number)
|
|
5
|
+
const pa = a.split('+')[0].split('.').map(Number)
|
|
6
|
+
const pb = b.split('+')[0].split('.').map(Number)
|
|
7
7
|
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
8
8
|
const diff = (pa[i] || 0) - (pb[i] || 0)
|
|
9
9
|
if (diff !== 0) return diff
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harveyz-skill",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "Skill manager for Claude Code, Cursor, Codex, OpenClaw, Hermes, OpenCode, and Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -62,6 +62,12 @@
|
|
|
62
62
|
"skills/coding/handoff/",
|
|
63
63
|
"skills/feed/manage-creators/",
|
|
64
64
|
"skills/feed/capture-opinion/",
|
|
65
|
+
"skills/agent-canvas/agent-canvas-control/",
|
|
66
|
+
"skills/agent-canvas/describe-node/",
|
|
67
|
+
"skills/agent-canvas/relate-node/",
|
|
68
|
+
"skills/agent-canvas/capture-requirement/",
|
|
69
|
+
"skills/agent-canvas/relation-review/",
|
|
70
|
+
"skills/agent-canvas/close-node/",
|
|
65
71
|
"tools/hub/",
|
|
66
72
|
"tools/sync-agent/",
|
|
67
73
|
"tools/browser-fetch/",
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-canvas-control
|
|
3
|
+
description: 在 Agent Canvas 的 claude-code/codex/pi 节点里操控节点实体与画布摆放(创建/查询/修改/删除节点、把节点放上或收起画布);也可在 shell/hermes-tui 节点内或画布外的终端里用 `--canvas`/`resolve-canvas`/`resolve` 操控
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 操控节点与画布(agent-canvas-control)
|
|
8
|
+
|
|
9
|
+
如果 `agent-canvas-ctl`/`agent-canvas-arrange` 命令都不存在,这个 skill 不适用,直接跳过,
|
|
10
|
+
不要尝试执行下面的命令。命令存在时可以直接尝试子命令——即使没有 `AGENT_CANVAS_MCP_URL`,
|
|
11
|
+
两个二进制也会按当前工作目录反查所在画布;如果解析不到目标画布,子命令会以非零退出码报错,
|
|
12
|
+
此时同样说明当前不适用,跳过即可,不要重试。
|
|
13
|
+
|
|
14
|
+
两个二进制分工**互不重叠**:`agent-canvas-ctl` 管节点实体与关系(建/查/改/删/连接),
|
|
15
|
+
`agent-canvas-arrange` 管画布摆放(放上/收起/移动/分组的成员与配色)——**建的节点默认不
|
|
16
|
+
放上画布**,节点存在但游离于画布之外,是正常状态,不是遗漏;想让它出现在画布上,建完之后
|
|
17
|
+
再调一次 `agent-canvas-arrange put`。两者所有子命令成功时把结果 JSON 打印到 stdout、退出码
|
|
18
|
+
0;失败时把错误信息打印到 stderr、退出码非 0。
|
|
19
|
+
|
|
20
|
+
## agent-canvas-ctl:节点实体与关系
|
|
21
|
+
|
|
22
|
+
- `agent-canvas-ctl list-nodes`
|
|
23
|
+
列出当前画布所有节点,返回 `[{id, title, description, kind, state}, ...]`(不含画布摆放
|
|
24
|
+
字段——查节点在不在画布上、在哪个坐标,用下面 `agent-canvas-arrange list/get`)。
|
|
25
|
+
|
|
26
|
+
- `agent-canvas-ctl get-node <nodeId>`
|
|
27
|
+
获取单个节点详情(同样不含摆放字段)。
|
|
28
|
+
|
|
29
|
+
- `agent-canvas-ctl search-nodes <query> [--limit <n>]`
|
|
30
|
+
按标题/描述模糊搜索当前所有非归档节点(编辑距离容错,不要求精确匹配,标题命中权重高于描述),
|
|
31
|
+
按相关度降序返回 `[{id, title, description, kind, state, createdAt, score}, ...]`。
|
|
32
|
+
`score` 不是归一化到 0~1 的置信度(标题命中会乘 1.5 倍权重),只用于本次结果内部排序。
|
|
33
|
+
`--limit` 缺省时不截断结果数量。收起的节点(`agent-canvas-arrange hide`)仍会被搜到;
|
|
34
|
+
已归档节点不在范围内,要搜归档节点用 `search-archived-nodes`。
|
|
35
|
+
|
|
36
|
+
- `agent-canvas-ctl create-node --kind <markdown|html|text|browser> --title <标题> --file <文件路径或URL> [--description <描述>]`
|
|
37
|
+
创建一个静态节点,**不放上画布**(不接受 `--position`)。`--file` 对 `markdown`/`html`/`text`
|
|
38
|
+
指向一个文件:`markdown` 在文件不存在时会创建空文件,`html`/`text` 要求文件已存在(否则
|
|
39
|
+
报错,不创建节点);对 `browser` 传入初始 URL(不做存在性校验)。成功返回 `{nodeId}`;
|
|
40
|
+
要放上画布用 `agent-canvas-arrange put <nodeId> [--position x,y]`。
|
|
41
|
+
|
|
42
|
+
- `agent-canvas-ctl create-pty-node --node-type <claude-code|shell|hermes-tui|codex> --title <标题> [--cwd <路径>] [--task-type <execute|evaluate|analyze|plan>]`
|
|
43
|
+
创建并启动一个 pty 节点,使用该节点类型的默认启动命令(不支持自定义 command);**不放上
|
|
44
|
+
画布**(不接受 `--position`,进程照常启动,与是否在画布上无关)。`--cwd` 缺省时用当前
|
|
45
|
+
shell 的工作目录。`claude-code` 类型总是新建会话,不支持 resume。`--task-type` 可选,
|
|
46
|
+
设置节点的初始任务类型分类。不支持 `customNodeTypes`。成功返回 `{nodeId}`。
|
|
47
|
+
|
|
48
|
+
- `agent-canvas-ctl create-requirement-node --title <标题> [--description <描述>] [--priority low|medium|high] [--status open|in_progress|done] [--acceptance-criteria <验收标准>] [--links <a,b,c>]`
|
|
49
|
+
创建一个需求节点,**不放上画布**。`--priority`/`--status` 缺省时分别为 `medium`/`open`。
|
|
50
|
+
成功返回 `{nodeId, requirementId}`——`requirementId` 是自动生成的需求编号(`YYMMDD-XX`
|
|
51
|
+
格式),和其余 `create-*` 子命令(只返回 `{nodeId}`)不同。
|
|
52
|
+
|
|
53
|
+
- `agent-canvas-ctl derive-pty-from-requirement <nodeId> [--cwd <路径>] [--node-type <claude-code|shell|hermes-tui|codex>] [--task-type <execute|evaluate|analyze|plan>]`
|
|
54
|
+
从一个需求节点派生出一个 pty 会话节点并立即启动进程。需求节点**原样保留**,派生出的
|
|
55
|
+
会话节点自动连一条 `implements` 关系指向它;同一条需求可以派生多个会话(分析、执行、
|
|
56
|
+
评估各一个)。派生出的节点**不放上画布**。
|
|
57
|
+
`--node-type` 缺省 `claude-code`,`--cwd` 缺省用当前 shell 的工作目录。
|
|
58
|
+
成功返回 `{nodeId, requirementNodeId, relationId}`。
|
|
59
|
+
|
|
60
|
+
- `agent-canvas-ctl update-node-title <nodeId> <新标题>`
|
|
61
|
+
修改节点标题。
|
|
62
|
+
|
|
63
|
+
- `agent-canvas-ctl update-node-description <nodeId> <新描述>`
|
|
64
|
+
修改节点描述。
|
|
65
|
+
|
|
66
|
+
- `agent-canvas-ctl update-node-task-type <nodeId> <execute|evaluate|analyze|plan|none>`
|
|
67
|
+
修改 pty 节点的任务类型分类,`none` 清空为未设置。仅对 pty 节点生效。
|
|
68
|
+
|
|
69
|
+
- `agent-canvas-ctl update-node-requirement <nodeId> [--status open|in_progress|verifying|done] [--deferred true|false] [--priority low|medium|high] [--acceptance-criteria <验收标准>] [--links a,b,c]`
|
|
70
|
+
修改一个已存在的需求节点的 requirement 字段,patch 语义:只有显式传了的字段才写,没传的
|
|
71
|
+
原样不动。`--deferred` 与 `--status` 正交,不是 status 的取值之一。仅对需求节点生效。
|
|
72
|
+
成功返回 `{success: true, requirement: <patch 后的完整 requirement 对象>}`。
|
|
73
|
+
|
|
74
|
+
- `agent-canvas-ctl archive-node <nodeId>`
|
|
75
|
+
归档节点(从检索空间移出+停进程,可恢复,不影响 relation)。
|
|
76
|
+
|
|
77
|
+
- `agent-canvas-ctl stop-node <nodeId>`
|
|
78
|
+
停止指定 pty 节点的底层进程。不归档、不删除、不改变它在画布上的摆放。对非 pty 节点
|
|
79
|
+
或进程已停止的节点是 no-op,如实回报 `stopped:false`,不报错。
|
|
80
|
+
成功返回 `{success: true, stopped: boolean, reason?: string}`。
|
|
81
|
+
|
|
82
|
+
- `agent-canvas-ctl purge-node <nodeId> --yes`
|
|
83
|
+
彻底删除节点(不可恢复,相关 relation 转 stale);`--yes` 必填,否则直接拒绝执行、
|
|
84
|
+
不发起任何画布请求。若在画布节点内调用,可能阻塞最长约 90 秒等待人工在该节点卡片上
|
|
85
|
+
审批;被拒绝时报错 `用户拒绝了该操作`。
|
|
86
|
+
|
|
87
|
+
- `agent-canvas-ctl list-archived-nodes`
|
|
88
|
+
列出当前所有已归档节点的摘要,返回 `[{id, title, description, kind, archivedAt}, ...]`。
|
|
89
|
+
|
|
90
|
+
- `agent-canvas-ctl search-archived-nodes <query>`
|
|
91
|
+
按关键字搜索已归档节点(大小写不敏感,匹配 title/description 子串),返回结构同上。
|
|
92
|
+
|
|
93
|
+
- `agent-canvas-ctl restore-node <nodeId>`
|
|
94
|
+
把一个已归档节点恢复回画布。
|
|
95
|
+
|
|
96
|
+
- `agent-canvas-ctl list-relations`
|
|
97
|
+
列出当前画布所有关系(含 stale),返回 `[{id, type, from, to, status, label}, ...]`。
|
|
98
|
+
|
|
99
|
+
- `agent-canvas-ctl link-nodes --from <nodeId> --to <nodeId> --type <t> [--label <文字>]`
|
|
100
|
+
在两节点间创建一条关系。`--type` 按两端节点 kind 的注册表校验(不合法组合报错并
|
|
101
|
+
列出该端点对的合法 type;`spawns` 是系统专属,总是被拒;`unspecified` 不能显式传)。
|
|
102
|
+
类型不是固定 7 个——注册表可演进,实际合法列表以 `agent-canvas-ctl relation-guide`
|
|
103
|
+
当前返回的为准。成功返回 `{relationId}`。
|
|
104
|
+
|
|
105
|
+
- `agent-canvas-ctl link-nodes --from <nodeId> --to <nodeId> --reading "<自然语言谓语>" --why "<为什么现有 type 都装不下>" [--despite <t>,<t>] [--label <文字>]`
|
|
106
|
+
**逃逸阀**:矩阵里这对端点确实没有一个合适的 type 时用这个,不传 `--type`。
|
|
107
|
+
`--reading`/`--why` 成对必填,缺一即拒。若该端点对已有现成的合法 type,还必须传
|
|
108
|
+
`--despite`,逐个点名已经看过、确认装不下的现有 type,漏一个即拒——这是留给治理循环的
|
|
109
|
+
"已排除记录",也是为了不让逃逸阀变成偷懒的默认选项。落盘后 `type` 记为 `unspecified`,
|
|
110
|
+
`reading`/`why` 单独存在关系上,将来治理循环可能把它升格成正式 type(不影响你现在
|
|
111
|
+
已经建的这条关系的 `relationId`)。
|
|
112
|
+
|
|
113
|
+
- `agent-canvas-ctl unlink-relation <relationId>`
|
|
114
|
+
删除一条关系(relationId 由 list-relations 获取)。若在画布节点内调用,可能阻塞
|
|
115
|
+
最长约 90 秒等待人工在该节点卡片上审批;被拒绝时报错 `用户拒绝了该操作`。
|
|
116
|
+
|
|
117
|
+
- `agent-canvas-ctl node-relations <nodeId> [--direction in|out|both] [--type <t>] [--depth <n>]`
|
|
118
|
+
返回该节点的关系,每条带对端摘要(`id`/`title`/`kind`)。`direction` 缺省 `both`;
|
|
119
|
+
`depth` 缺省 1、上限 3(超过会被夹到 3,不报错),带环检测(A→B→A 不会无限展开)。
|
|
120
|
+
|
|
121
|
+
- `agent-canvas-ctl update-relation <relationId> [--type <t>] [--label <文字>]`
|
|
122
|
+
修改一条已有关系的 type/label(不支持改 status/display)。`--type`/`--label` 至少传一个;
|
|
123
|
+
改 type 时同样按矩阵校验。
|
|
124
|
+
|
|
125
|
+
- `agent-canvas-ctl relation-guide [<nodeId>]`
|
|
126
|
+
返回关系矩阵、判据、阶段建议,以及当前节点的上下文(既有关系/血缘链/需求节点候选/
|
|
127
|
+
文档节点候选)。不传 `nodeId` 时按祖先进程链反查调用者自己所在的节点(同
|
|
128
|
+
`whoami`/`summarize-self`)。「什么时候该调、该建哪些关系」见 `relate-node` skill。
|
|
129
|
+
|
|
130
|
+
- `agent-canvas-ctl create-group [--title <标题>] [--description <描述>]`
|
|
131
|
+
只建分组容器实体,**不放上画布、不加成员、不配色**。成功返回 `{nodeId}`;接下来用
|
|
132
|
+
`agent-canvas-arrange put` 摆上画布,再用 `group-add`/`group-color` 加成员/配色
|
|
133
|
+
(加成员要求分组已经在画布上,是正当前置条件,不是限制)。
|
|
134
|
+
|
|
135
|
+
- `agent-canvas-ctl whoami`
|
|
136
|
+
反查当前进程所属的画布节点,返回 `{id, title, description, kind, state}`。
|
|
137
|
+
只在画布节点的进程树内可用。
|
|
138
|
+
|
|
139
|
+
- `agent-canvas-ctl summarize-self [--spec <规格文档路径>]`
|
|
140
|
+
为你所在的这个节点生成标题、描述与任务类型并写回画布,不需要传 nodeId。
|
|
141
|
+
`--spec` 指定据以撰写的规格文档;不传时会从本会话记录里自动发现
|
|
142
|
+
(`docs/superpowers/specs/` 下你写过或读过的 .md)。
|
|
143
|
+
返回 `{nodeId, title, description, taskType?, specUsed}`;
|
|
144
|
+
`specUsed` 为 null 表示没找到规格文档,如有需要可用 `--spec` 补传。
|
|
145
|
+
不再撰写需求——需求撰写是独立的 `draft-requirement`(下一条)。该命令要跑一次模型
|
|
146
|
+
调用,通常 5–15 秒。
|
|
147
|
+
|
|
148
|
+
- `agent-canvas-ctl draft-requirement [--spec <规格文档路径>]`
|
|
149
|
+
为你正在实现的某个需求节点撰写标题/描述/优先级/状态/验收标准,纯撰写不落盘。
|
|
150
|
+
`--spec` 指定据以撰写的规格文档;不传时会从本会话记录里自动发现
|
|
151
|
+
(`docs/superpowers/specs/` 下你写过或读过的 .md)。
|
|
152
|
+
该命令只应被 `capture-requirement` skill 的三步流水线调用,不独立暴露给用户。
|
|
153
|
+
节点已 implements 某个需求节点、或未读到 spec 时以非零退出码报错(需求编号不重铸)。
|
|
154
|
+
|
|
155
|
+
- `agent-canvas-ctl notify <text> [--title <标题>]`
|
|
156
|
+
往通知中心发一条消息(不经过画布工具体系,不是画布操控)。落一条项目 scope 的通知,
|
|
157
|
+
能反查到调用节点时通知会关联到该节点。`--title` 缺省为 "PTY"。
|
|
158
|
+
|
|
159
|
+
- `agent-canvas-ctl list-profiles`
|
|
160
|
+
列出当前可见的全部 Agent Profile(含全局与项目两个 scope,project 覆盖同 id 的 global)。
|
|
161
|
+
|
|
162
|
+
- `agent-canvas-ctl create-profile --json <文件路径>`
|
|
163
|
+
创建一个新的 Agent Profile。`--json` 指向一个 JSON 文件,内容需包含 `name`/`scope`
|
|
164
|
+
(`project`|`global`)等字段;`id` 缺省时自动生成,传了已存在的 `id` 会报错。
|
|
165
|
+
|
|
166
|
+
- `agent-canvas-ctl update-profile --json <文件路径>`
|
|
167
|
+
更新一个已存在的 Agent Profile。`--json` 指向一个 JSON 文件,内容需包含 `id`;
|
|
168
|
+
`runtime`/`workingDir` 不可修改——传了不同值会报错。
|
|
169
|
+
|
|
170
|
+
- `agent-canvas-ctl resolve-canvas`
|
|
171
|
+
只做目标画布解析、不连 MCP server。见下方「目标画布解析」。
|
|
172
|
+
|
|
173
|
+
## agent-canvas-arrange:画布摆放
|
|
174
|
+
|
|
175
|
+
节点建完默认不在画布上——这不是缺可见性,节点列表照常能看到它、能从那里一键放上画布;
|
|
176
|
+
这里是给 agent 用的等价操作。
|
|
177
|
+
|
|
178
|
+
- `agent-canvas-arrange list [--on-canvas | --off-canvas | --hidden]`
|
|
179
|
+
列出全部节点的摆放态(含不在画布上的),返回
|
|
180
|
+
`[{id, title, kind, onCanvas, hidden, position?, width?, height?, containerId?, color?}, ...]`。
|
|
181
|
+
三个过滤参数互斥,缺省返回全集;`--on-canvas`/`--off-canvas` 按有没有落在画布上筛,
|
|
182
|
+
`--hidden` 单独筛"放过又收起"的节点。
|
|
183
|
+
|
|
184
|
+
- `agent-canvas-arrange search <query> [--limit <n>]`
|
|
185
|
+
与 `agent-canvas-ctl search-nodes` 同一套模糊匹配,返回摆放形状(带 `score`)。
|
|
186
|
+
含收起的节点,不含已归档节点。**这是「自足」的关键**:只用这一个二进制就能从
|
|
187
|
+
"标题里带某个词的节点"走到"把它摆上画布",不需要回 `agent-canvas-ctl` 拿 id。
|
|
188
|
+
|
|
189
|
+
- `agent-canvas-arrange get <nodeId>`
|
|
190
|
+
单个节点的摆放态,形状与 `list` 里的一条一致。
|
|
191
|
+
|
|
192
|
+
- `agent-canvas-arrange put <nodeId1,nodeId2,...> [--position <x,y>]`
|
|
193
|
+
把节点放上画布。不带 `--position` 时用自动布局位(可以是逗号分隔的多个 id 一次放上);
|
|
194
|
+
带 `--position` 时精确定位到该坐标(只能对单个 id 生效)。已经在画布上的节点是 no-op。
|
|
195
|
+
|
|
196
|
+
- `agent-canvas-arrange hide <nodeId1,nodeId2,...>`
|
|
197
|
+
将指定节点从画布隐藏:仅影响呈现层,不影响该节点参与检索、图搜索的能力,可随时通过
|
|
198
|
+
`put` 恢复显示。对没有放上画布过的节点是 no-op。
|
|
199
|
+
|
|
200
|
+
- `agent-canvas-arrange move <nodeId> <x> <y>`
|
|
201
|
+
把已在画布上的节点移动到指定坐标(等价于 `put <nodeId> --position <x,y>`,位置参数写法
|
|
202
|
+
不同:这里 x/y 是独立参数,`put` 是 `--position x,y`)。
|
|
203
|
+
|
|
204
|
+
- `agent-canvas-arrange group-add <groupId> <nodeId1,nodeId2,...>`
|
|
205
|
+
把节点加入分组。**分组必须已经在画布上**(先对 groupId 调过 `put`),否则响亮报错、
|
|
206
|
+
非零退出——不是静默失败。
|
|
207
|
+
|
|
208
|
+
- `agent-canvas-arrange group-remove <groupId> <nodeId1,nodeId2,...>`
|
|
209
|
+
把节点移出分组。若分组是网格布局,被移除节点在 `memberCells` 里占的格子一并清掉。
|
|
210
|
+
|
|
211
|
+
- `agent-canvas-arrange group-color <groupId> <颜色>`
|
|
212
|
+
修改分组容器的配色。颜色取值:`red`/`orange`/`yellow`/`green`/`teal`/`blue`/
|
|
213
|
+
`indigo`/`purple`/`pink`/`black`/`gray`/`white`。
|
|
214
|
+
|
|
215
|
+
- `agent-canvas-arrange whoami`
|
|
216
|
+
与 `agent-canvas-ctl whoami` 是同一个查询,两边都有(寻址前置,不专属任何一域)。
|
|
217
|
+
|
|
218
|
+
- `agent-canvas-arrange resolve`
|
|
219
|
+
与 `agent-canvas-ctl resolve-canvas` 是同一套本地解析,只是名字更短。见下方
|
|
220
|
+
「目标画布解析」。
|
|
221
|
+
|
|
222
|
+
## 目标画布解析
|
|
223
|
+
|
|
224
|
+
上面所有子命令默认按以下优先级自动定位画布,通常不需要关心这一节:
|
|
225
|
+
`--canvas <路径>`(放在子命令之前)> 环境变量 `AGENT_CANVAS_MCP_URL`(画布内节点自带)>
|
|
226
|
+
当前工作目录向上匹配注册表里画布的绑定路径。
|
|
227
|
+
|
|
228
|
+
`agent-canvas-ctl resolve-canvas` / `agent-canvas-arrange resolve` 只做目标画布解析、
|
|
229
|
+
不连 MCP server。解析到时把 `{url, source, boundPath?, pid?}` 打印到 stdout、退出码 0;
|
|
230
|
+
解析不到时把可用画布清单打印到 stderr、退出码 1。适合在执行别的子命令前先探测
|
|
231
|
+
「现在有没有画布可连」。
|
|
232
|
+
|
|
233
|
+
## 已知限制
|
|
234
|
+
|
|
235
|
+
不支持触发节点的"功能按钮"(restart/branch/stop 等),只有增删改查与摆放。
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: capture-requirement
|
|
3
|
+
description: 把当前会话正在实现的需求立项成一个独立的需求节点,并挂一条 implements 关系到自己身上——闸门2定稿后调用一次;仅在画布节点里可用
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 需求立项(capture-requirement)
|
|
8
|
+
|
|
9
|
+
如果当前不在 Agent Canvas 的画布节点里(即环境变量 `AGENT_CANVAS_MCP_URL` 未设置,
|
|
10
|
+
或 `agent-canvas-ctl` 命令不存在),这个 skill 不适用,直接跳过,不要尝试执行下面的命令。
|
|
11
|
+
|
|
12
|
+
## 怎么做(三步,每步一个既有/新增的单一能力命令)
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
1. agent-canvas-ctl draft-requirement [--spec <绝对路径>]
|
|
16
|
+
→ { callerNodeId, title, description, priority, status, acceptanceCriteria?, specUsed }
|
|
17
|
+
|
|
18
|
+
2. agent-canvas-ctl create-requirement-node \
|
|
19
|
+
--title <上一步的 title> --description <上一步的 description> \
|
|
20
|
+
--priority <上一步的 priority> --status <上一步的 status> \
|
|
21
|
+
[--acceptance-criteria <上一步的 acceptanceCriteria>]
|
|
22
|
+
→ { nodeId, requirementId }
|
|
23
|
+
|
|
24
|
+
3. agent-canvas-ctl link-nodes \
|
|
25
|
+
--from <第 1 步的 callerNodeId> --to <第 2 步的 nodeId> --type implements
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
第 1 步**只撰写、不落盘**——`--spec` 不传时按本会话记录自动发现,必须是绝对路径。
|
|
29
|
+
第 1 步以非零退出码报错时,说明这三种情况之一:没读到 spec、本节点已 implements 过某个
|
|
30
|
+
需求节点(不重铸编号)、或本节点上一次调用还没返回;报错本身已说明是哪一种,直接把错误
|
|
31
|
+
如实报出,不要重试或跳过。
|
|
32
|
+
|
|
33
|
+
## 孤儿处置
|
|
34
|
+
|
|
35
|
+
**第 3 步失败时,立刻执行 `agent-canvas-ctl purge-node <第 2 步的 nodeId> --yes`**,收掉
|
|
36
|
+
第 2 步建出来的需求节点,并把失败原因如实报出。不要留着第 2 步建出来的孤儿节点不处理。
|
|
37
|
+
|
|
38
|
+
## 什么时候调
|
|
39
|
+
|
|
40
|
+
**只在闸门 2 定稿后调一次**,不在收尾时调——需求正文在 spec 定稿那一刻最准确,收尾时会话
|
|
41
|
+
上下文已被实现细节填满,重写只会把需求拧回实现叙述。与 `implements` 关系"一次写入不可
|
|
42
|
+
覆盖"的语义一致:本节点已 implements 某个需求节点时,第 1 步会直接拒绝。
|
|
43
|
+
|
|
44
|
+
handoff 场景:交出方在闸门 2 调(需求已立项);接手方**不调**——需求节点已经在画布上,
|
|
45
|
+
重复立项没有意义。注意第 1 步的前置检查只认**本节点自己**的 implements 边,接手方是一个
|
|
46
|
+
全新节点、没有这条边,检查不会自动拦下重复调用;不调纯粹是约定,不是系统会替你挡住。
|
|
47
|
+
|
|
48
|
+
## 边界
|
|
49
|
+
|
|
50
|
+
不是 `describe-node` 的替代——`describe-node` 管本节点自己的 title/description/taskType,
|
|
51
|
+
本 skill 管"另一个"需求节点的立项。两者常常紧挨着调,但各自独立,互不覆盖对方的产物。
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: close-node
|
|
3
|
+
description: 一轮工作做完时给当前画布节点收尾——推进需求状态、核对 handoff 收口、合并分支、停进程并从画布收起;仅在画布节点里可用
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
version: "1.0.0"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# 节点收尾(close-node)
|
|
9
|
+
|
|
10
|
+
如果当前不在 Agent Canvas 的画布节点里(即环境变量 `AGENT_CANVAS_MCP_URL` 未设置,
|
|
11
|
+
或 `agent-canvas-ctl` 命令不存在),这个 skill 不适用,直接跳过,不要尝试执行下面的命令。
|
|
12
|
+
|
|
13
|
+
**人工触发**,不自动触发。触发时机:你这一轮工作已经做完、已经汇报过、确认不再有后续动作。
|
|
14
|
+
|
|
15
|
+
## 步骤
|
|
16
|
+
|
|
17
|
+
### 1. 认清自己
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
agent-canvas-ctl whoami → 自己的 nodeId
|
|
21
|
+
agent-canvas-ctl node-relations <自己的 nodeId> → implements 边 / hands-off-to 边
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 2. 判角色
|
|
25
|
+
|
|
26
|
+
在 `docs/commute/` 下找 frontmatter 的 `source_node` 或 `target_node` 等于自己 nodeId
|
|
27
|
+
的交接文档:
|
|
28
|
+
|
|
29
|
+
| 角色 | 判据 |
|
|
30
|
+
|---|---|
|
|
31
|
+
| **独立节点** | 没有 `hands-off-to` 边,也没有一份交接文档点名自己 |
|
|
32
|
+
| **交出方** | 自己的 nodeId == 某份交接文档的 `source_node` |
|
|
33
|
+
| **接手方** | 自己的 nodeId == 某份交接文档的 `target_node` |
|
|
34
|
+
|
|
35
|
+
"验收方"不是第四类——`/handoff` 的 accept 阶段由原 session 做,验收方就是交出方本人。
|
|
36
|
+
|
|
37
|
+
### 3. 推进需求状态
|
|
38
|
+
|
|
39
|
+
沿 `implements` 边找到需求节点。判断它现在该是什么状态,然后:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
agent-canvas-ctl update-node-requirement <需求节点id> --status <verifying|done>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- 活干完了、还没人验 → `verifying`
|
|
46
|
+
- 已经验收通过 → `done`
|
|
47
|
+
- 这条需求要搁置(`deferred` 与 `status` 正交,不是它的取值)→ `--deferred true`
|
|
48
|
+
- **拿不准就别改**,如实报告"需求状态维持 X,因为 Y"。改错了比不改更难发现。
|
|
49
|
+
|
|
50
|
+
没有 `implements` 边 → 跳过这一步,不要顺手立项一个需求。
|
|
51
|
+
|
|
52
|
+
### 4. handoff 收口
|
|
53
|
+
|
|
54
|
+
**独立节点**:跳过,直接进第 5 步。
|
|
55
|
+
|
|
56
|
+
**接手方**:
|
|
57
|
+
- 核对交接文档 frontmatter 的 `status` 是否已置 `待验收`,没置就置上。
|
|
58
|
+
- 核对 `hands-off-to` 边两端是否都齐、`target_node` 是否已回填自己的 nodeId。
|
|
59
|
+
- **不合并、不 `git worktree remove`。** 要离开用 `ExitWorktree(action: "keep")`。
|
|
60
|
+
|
|
61
|
+
**交出方**:
|
|
62
|
+
- 核对交接文档 `status` 是否已到 `已验收`。
|
|
63
|
+
- **没到 `已验收` 就停在这里报告,不要继续往下走。** 收尾不等于验收通过。
|
|
64
|
+
- 到了 `已验收` 之后,再核对本分支是否已经合并进 staging
|
|
65
|
+
(`git merge-base --is-ancestor HEAD staging` 成功即已合并)。
|
|
66
|
+
**未合并就停在这里报告,不要进第 7 步。** 合并是 `/handoff` accept 阶段由你自己跑
|
|
67
|
+
`scripts/merge-to-staging.sh` 的动作,本 skill 不代跑;把没合并的分支连同节点一起
|
|
68
|
+
关掉,等于把工作弄丢了。
|
|
69
|
+
|
|
70
|
+
> **只有交出方能合并、能 `git worktree remove`。** 这是硬约束,不是建议。
|
|
71
|
+
|
|
72
|
+
### 5. 合并(仅独立节点)
|
|
73
|
+
|
|
74
|
+
先确认 `scripts/merge-to-staging.sh` 存在、且当前仓库确实采用本 skill 描述的这套流程。
|
|
75
|
+
不存在就说明本仓库不适用这一步,**停下报告,不要自行换用别的合并手法**。
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
git rev-parse --abbrev-ref HEAD # 先核对自己站在哪条分支上
|
|
79
|
+
cd "$(git rev-parse --show-toplevel)" # cwd 未必在仓库根,先切过去
|
|
80
|
+
scripts/merge-to-staging.sh
|
|
81
|
+
git worktree remove <本工作区>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
脚本会拒绝在 staging/main 上执行,但它拦不住"你以为在工作区里、其实在主工作树上"。
|
|
85
|
+
|
|
86
|
+
若 `git worktree remove` 提示有未提交改动,**停下报告,不要加 `--force`**。
|
|
87
|
+
|
|
88
|
+
**合并失败就停在这里报告,不要进第 7 步。** 把没合并的分支连同节点一起关掉,等于把工作弄丢了。
|
|
89
|
+
|
|
90
|
+
### 6. 汇报
|
|
91
|
+
|
|
92
|
+
把上面每一步做了什么、跳过了什么、为什么,一次说完。
|
|
93
|
+
|
|
94
|
+
> **第 7 步一执行就没有输出了。所有要给人看的东西必须在这里说完。**
|
|
95
|
+
|
|
96
|
+
### 7. 关闭自己
|
|
97
|
+
|
|
98
|
+
先自查:有没有 subagent 还在后台跑?有就等它们完成,不要现在关。
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
agent-canvas-arrange hide <自己的 nodeId>
|
|
102
|
+
agent-canvas-ctl stop-node <自己的 nodeId>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**顺序不能反。** `stop-node` 一执行进程就没了,排在它后面的命令发不出去。
|
|
106
|
+
|
|
107
|
+
这一步之后没有任何输出是**预期行为**,不是故障。
|
|
108
|
+
|
|
109
|
+
## 边界
|
|
110
|
+
|
|
111
|
+
- **不代跑 `describe-node`、`relate-node`。** 那两个的第二次调用仍按 CLAUDE.md 由你自己
|
|
112
|
+
在收尾前调,本 skill 与它们**并列**,不是它们的编排器。调本 skill 之前先把它们调完。
|
|
113
|
+
- **不调 `capture-requirement`。** 需求立项只在闸门 2 发生一次,收尾不重复立项。
|
|
114
|
+
- 不动 `implements` 边——只改需求节点自己的 `status`/`deferred`。
|
|
115
|
+
- `stop-node` 不走 UI 那套阻断策略(busy / 有活跃 subagent / 绑着 worktree)。
|
|
116
|
+
那三条是防人手滑的护栏,你显式调用是有意图的动作。**代价是这个工具很锋利**——
|
|
117
|
+
第 7 步的自查是唯一的缓解,机制不会替你挡。
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: describe-node
|
|
3
|
+
description: 为 Agent Canvas 中你所在的这个画布节点自动生成标题与描述——总结/摘要当前会话在做什么、给节点起名、更新节点标题或描述时使用;仅在画布节点里可用
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 为当前节点生成标题与描述(describe-node)
|
|
8
|
+
|
|
9
|
+
如果当前不在 Agent Canvas 的画布节点里(即环境变量 `AGENT_CANVAS_MCP_URL` 未设置,
|
|
10
|
+
或 `agent-canvas-ctl` 命令不存在),这个 skill 不适用,直接跳过,不要尝试执行下面的命令。
|
|
11
|
+
|
|
12
|
+
## 怎么调
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
agent-canvas-ctl summarize-self [--spec <规格文档路径>]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**不需要传节点 id**——它按祖先进程链反查你所在的节点,"谁调用就摘要谁"。
|
|
19
|
+
|
|
20
|
+
- `--spec` 指定据以撰写的规格文档。不传时会从本会话记录里自动发现你写过或读过的
|
|
21
|
+
`docs/superpowers/specs/*.md`;返回值里的 `specUsed` 为 null 就表示没找到,
|
|
22
|
+
如果你知道该用哪份,用 `--spec` 补传一次会明显更准。
|
|
23
|
+
- 成功时 stdout 返回 `{nodeId, title, description, taskType?, specUsed}`,退出码 0;
|
|
24
|
+
失败时错误写 stderr、退出码非 0。
|
|
25
|
+
- 要跑一次模型调用,通常 5–15 秒,属正常。
|
|
26
|
+
|
|
27
|
+
## 什么时候值得调
|
|
28
|
+
|
|
29
|
+
- **节点标题还是默认的类型名**(`Claude Code` / `Pi Agent` / `Shell`)时——这时画布上
|
|
30
|
+
同类节点长得一模一样,无法区分,最该起名。
|
|
31
|
+
- 刚写完或刚确定一份 spec 之后。
|
|
32
|
+
- 一个阶段性任务完成、会话主题已经明确之后。
|
|
33
|
+
- 用户明确要求"给这个节点起个名 / 写个描述 / 总结一下这个节点在做什么"。
|
|
34
|
+
|
|
35
|
+
## 什么时候不要调
|
|
36
|
+
|
|
37
|
+
- 会话刚开始、还没有任何实质内容时——没东西可摘,只会得到空泛的标题。
|
|
38
|
+
- 用户手动改过标题、而这一轮并没有要求重新生成时——**本命令会覆盖标题**。
|
|
39
|
+
- 同一节点上一次调用还没返回时——会直接报"正在进行中",等它完成即可。
|
|
40
|
+
|
|
41
|
+
## 边界
|
|
42
|
+
|
|
43
|
+
本命令**只产出 title/description/taskType**,不再撰写需求。若材料能清楚判断任务类型
|
|
44
|
+
(执行/评估/分析/计划),也会一并写入并覆盖已有值——taskType 不做"仅未设置时写"的保护,
|
|
45
|
+
即使是人工在面板下拉框里手动设置的值也会被覆盖。
|
|
46
|
+
|
|
47
|
+
需要把当前会话在实现的需求立项成一个节点,用 `capture-requirement` skill,不是本命令。
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: relate-node
|
|
3
|
+
description: 在 Agent Canvas 里为当前节点梳理与其他节点的语义关系(依赖/交接/实现需求/产出或引用文档)——闸门2定稿后、收尾汇报前,判断要不要给已有节点连一条关系时使用;仅在画布节点里可用
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 梳理节点关系(relate-node)
|
|
8
|
+
|
|
9
|
+
如果当前不在 Agent Canvas 的画布节点里(即环境变量 `AGENT_CANVAS_MCP_URL` 未设置,
|
|
10
|
+
或 `agent-canvas-ctl` 命令不存在),这个 skill 不适用,直接跳过,不要尝试执行下面的命令。
|
|
11
|
+
|
|
12
|
+
## 怎么做
|
|
13
|
+
|
|
14
|
+
1. 调 `agent-canvas-ctl relation-guide`(不需要传 nodeId,按祖先进程链反查自己),
|
|
15
|
+
拿到 `{ matrix, criterion, phase, context }`:
|
|
16
|
+
- `matrix`:`(fromKind, toKind)` 端点对允许的 type 全集,每项带 `reading`(怎么读,
|
|
17
|
+
type 名是谓语、from 是主语、to 是宾语)与 `agentAllowed`(false 的项——目前只有
|
|
18
|
+
`spawns`——是系统专属,不要尝试建)。
|
|
19
|
+
- `criterion`:判据原文,「不记它,将来某个具体动作会做错或做不了;写不出消费点就不记」。
|
|
20
|
+
- `phase.start` / `phase.finish`:当前所处阶段该核对哪些 type(见下方「两次调用」)。
|
|
21
|
+
- `context.node`:自己这个节点的 id/title/kind/cwd。
|
|
22
|
+
- `context.existingRelations`:自己已有的关系(含对端摘要)——**先看这个,避免重复主张**。
|
|
23
|
+
- `context.lineage`:spawns/hands-off-to 血缘链。
|
|
24
|
+
- `context.requirementNodes`:画布上的需求节点候选。
|
|
25
|
+
- `context.candidateDocs`:同 cwd 下的 markdown/html/text 节点候选。
|
|
26
|
+
2. **按 `phase` 对应的 type 列表逐个比对**(不是自由联想):对每个候选 type,问「`context`
|
|
27
|
+
里有没有一个具体的候选对象(需求节点/文档节点/血缘链上的节点),配得上这个 type 的读法」。
|
|
28
|
+
配不上就跳过,不要为了凑数硬建。
|
|
29
|
+
3. 对通过判据的每一条,调 `agent-canvas-ctl link-nodes --from <id> --to <id> --type <t> [--label <s>]`。
|
|
30
|
+
传矩阵外的组合会报错并回传该端点对的合法 type 列表。**如果连报错列出的合法 type 都没有
|
|
31
|
+
一个装得下**(不是"选一个将就",是确实没有语义相符的),改用
|
|
32
|
+
`agent-canvas-ctl link-nodes --from <id> --to <id> --reading "<谓语读法>" --why "<原因>" [--despite <已排除的type>,...]`
|
|
33
|
+
逃逸阀记下来,不要放弃这条主张也不要硬套一个不准确的现有 type。
|
|
34
|
+
4. 简短报告建了哪些关系、跳过了哪些候选及原因。
|
|
35
|
+
|
|
36
|
+
## 两次调用(对齐 describe-node 的两个时机,可紧挨着调)
|
|
37
|
+
|
|
38
|
+
- **第一次(闸门 2 定稿后、建分支前)**:核对 `phase.start` 列出的 type——
|
|
39
|
+
`implements`(需求此刻最显眼,收尾时早被压缩出上下文)、`depends-on`(已知的阻塞)、
|
|
40
|
+
`hands-off-to`(**只核对,不在这里新建**——接手方要补走 `/handoff` verify,见下方归属规则)。
|
|
41
|
+
- **第二次(收尾、汇报验收前)**:核对 `phase.finish` 列出的 type——
|
|
42
|
+
`produces`/`references`(文档要等到这时才成立)、`depends-on`(补漏)、
|
|
43
|
+
`hands-off-to`(这次要交出去、且已经能指认接手节点时才新建)。
|
|
44
|
+
|
|
45
|
+
## hands-off-to 的归属
|
|
46
|
+
|
|
47
|
+
**谁先能同时指认两端谁建**,另一方只核对、不重复主张(两边都建会出重复关系):
|
|
48
|
+
|
|
49
|
+
- 交出方在 `/handoff` author 阶段**已经能指认接手节点**(比如接手用的节点是它亲手创建的)
|
|
50
|
+
→ 当场建,并在交接文档里注明已建。
|
|
51
|
+
- 指认不到接手节点(常态:接手方是还不存在的下一个 session)→ 交出方只把自己的节点 id 写进
|
|
52
|
+
交接文档 frontmatter 的 `source_node`,由**接手方在 `/handoff` verify 阶段**读到它之后
|
|
53
|
+
建、并把自己的 id 回填 `target_node`。
|
|
54
|
+
- 建之前先看 `context.existingRelations` / `node-relations`,已有就跳过。
|
|
55
|
+
|
|
56
|
+
接手方补的只有这一条边:**不建 `implements`、不调 `capture-requirement`**——需求挂在交接源
|
|
57
|
+
节点上,接手节点该不该关联需求是另一个问题。
|
|
58
|
+
|
|
59
|
+
## 什么时候不要调
|
|
60
|
+
|
|
61
|
+
- 会话刚开始、还没有任何候选对象(需求/文档/上游节点)时。
|
|
62
|
+
- `context.existingRelations` 里已经有语义相同的关系时——不要重复主张。
|
|
63
|
+
- 矩阵里没有对应格子的端点对(比如两个 group 节点之间)——这是显式的空格子,不是遗漏。
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: relation-review
|
|
3
|
+
description: 全局 Pilot 专用的 relation 类型治理循环——扫描待议区、按四道关卡审议是否升格新
|
|
4
|
+
type;由 cron 定时调起,不应在项目画布节点里手动调用
|
|
5
|
+
version: "1.0.0"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Relation 类型治理循环(relation-review)
|
|
9
|
+
|
|
10
|
+
如果当前不是全局 Pilot 身份(`agent-canvas-ctl` 命令不存在,或明显运行在某个项目画布节点
|
|
11
|
+
的上下文里),这个 skill 不适用,直接跳过。
|
|
12
|
+
|
|
13
|
+
## 第一步:数待议区,决定要不要唤起完整审议
|
|
14
|
+
|
|
15
|
+
调 `agent-canvas-ctl relation-review-queue`,拿到全部画布里
|
|
16
|
+
`type='unspecified' && provenance='agent' && reading != null` 的关系列表,每条带
|
|
17
|
+
`{relationId, cwd, fromNodeId, toNodeId, fromKind, toKind, reading, why, createdAt}`。
|
|
18
|
+
|
|
19
|
+
**`WAKE_N = 5`**:本次列表长度**相比上次记录的基线**新增条目数 < 5 时,只记下这次的数量
|
|
20
|
+
(可以简单回复"待议区当前 N 条,未达唤起阈值,不审议"),**不要往下做任何关卡判断**——
|
|
21
|
+
空转成本压到这一次计数(spec §5.1)。达到阈值才继续。
|
|
22
|
+
|
|
23
|
+
## 第二步:按语义聚类
|
|
24
|
+
|
|
25
|
+
把待议区列表按"读法在说同一件事"聚类(不是按 `reading` 字面量分组——不同措辞可能是
|
|
26
|
+
同一个模式)。每一簇是一个候选 type 提案。
|
|
27
|
+
|
|
28
|
+
## 第三步:对每个候选簇过四道关卡
|
|
29
|
+
|
|
30
|
+
**审议时不要看任何"这条提案本来该不该过"的暗示**——你只应该看到 `relation-review-queue`
|
|
31
|
+
返回的原始数据,不要预设结论。每道关卡的产出必须引用真实 `relationId`,不能虚构案例;
|
|
32
|
+
写不满结构化字段就是没过这道关,不能用"综合考虑认为合理"糊过去。
|
|
33
|
+
|
|
34
|
+
### 关卡一 · 谓语关
|
|
35
|
+
|
|
36
|
+
判据:**把 `to` 端换成另一种 kind 的节点,这个 type 还成立吗?** 成立才是谓语,不成立
|
|
37
|
+
说明它描述的是宾语的种类(比如"这份文档是设计稿"这种,该由文档节点子类型承载,不是
|
|
38
|
+
新 relation type——这条岔路必须堵死,见 spec §1.3)。
|
|
39
|
+
|
|
40
|
+
打回时必须**明说是关卡一**,否则同一提案会反复被提上来。
|
|
41
|
+
|
|
42
|
+
### 关卡二 · 聚类关(用例门槛)
|
|
43
|
+
|
|
44
|
+
判据:同一语义在待议区累计 **≥ `PROMOTE_N`(3)条**,且 **来自 ≥2 个不同节点**
|
|
45
|
+
(`fromNodeId` 不同,不是同一个节点连记了好几遍——那是一个案例复述多遍,不是一个模式)。
|
|
46
|
+
|
|
47
|
+
不满足就打回,附上当前累计数量和涉及的节点数,方便下次审议时快速核对是否已经攒够。
|
|
48
|
+
|
|
49
|
+
### 关卡三 · 可辨别性关
|
|
50
|
+
|
|
51
|
+
判据:对该端点对上现有的**每一个** type(用 `agent-canvas-ctl relation-guide` 查该端点对
|
|
52
|
+
当前合法的 type 列表),举出一个真实 `relationId`,说明在那条具体的关系上,新 type 与
|
|
53
|
+
这个现有 type 会给出不同答案。写进最终提案的 `distinguishedFrom` 字段。
|
|
54
|
+
|
|
55
|
+
**有一个现有 type 举不出区分案例,就不是新 type,是那个已有 type 的实例**——打回,
|
|
56
|
+
并建议把这批关系改用 `agent-canvas-ctl update-relation <relationId> --type <已有type>`
|
|
57
|
+
归到它名下(不要自己批量执行,写进审议报告让下一步的人工事后核查时看到)。
|
|
58
|
+
|
|
59
|
+
每条候选自带的 `why` 字段(agent 走 `link-nodes` 逃逸阀建这条关系时就已经写明"为什么
|
|
60
|
+
现有 type 装不下")是关卡三最直接的原料,优先核对这些——**不是 `despite`**:`despite`
|
|
61
|
+
只在写入那一刻用于校验"逐个点名已排除的现有 type",校验完就丢弃,不落在 Relation 记录上、
|
|
62
|
+
`relation-review-queue` 也不会把它吐出来,审议阶段读不到。
|
|
63
|
+
|
|
64
|
+
### 关卡四 · 消费关(软)
|
|
65
|
+
|
|
66
|
+
判据:写出至少一个"谁会读它"——一句具体的提问(例如"这个需求被哪个取代了"),
|
|
67
|
+
允许答案是"agent 通过 `get_node_relations`/`relation-guide` 读回"(现有 7 个 type 里
|
|
68
|
+
已有 4 个只有这个消费点,不是更高标准)。一句都写不出就打回。
|
|
69
|
+
|
|
70
|
+
## 第四步:对通过全部四关的提案执行升格
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
agent-canvas-ctl relation-promote \
|
|
74
|
+
--name <新type名> \
|
|
75
|
+
--reading "<谓语读法>" \
|
|
76
|
+
--endpoints "<fromKind>:<toKind>,<fromKind>:<toKind>" \
|
|
77
|
+
--evidence "<cwd>::<relationId>,<cwd>::<relationId>,..." \
|
|
78
|
+
--gate-reasoning "<四道关卡论证全文,含每一关的判断依据>"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`--endpoints` 里的 `<fromKind>`/`<toKind>` 是节点 **kind**(比如 `pty`/
|
|
82
|
+
`requirement`),不是节点 id——跟 `--evidence` 里的 `relationId` 是两种不同的标识符,
|
|
83
|
+
不要混用。`--evidence` 列出的应该是这一簇里全部符合条件的 `relationId`(不止 3 条门槛
|
|
84
|
+
数量,攒了多少条证据就列多少条——升格后这些关系的 `type` 会被原样改写成新 type 名)。
|
|
85
|
+
|
|
86
|
+
对没通过的提案,**不调用任何写操作**,只在报告里说明打回在哪一关、原因是什么。
|
|
87
|
+
|
|
88
|
+
## 第四步之后:审议中顺带发现的其他治理动作
|
|
89
|
+
|
|
90
|
+
四道关卡只回答"这个候选该不该升格成新 type"。审议过程中如果顺带发现**已经是正式 type**
|
|
91
|
+
的词条有下面三类问题,同一轮一并处理(不需要单独等下一轮)——这三个动作**不改任何存量
|
|
92
|
+
relation 数据**,只改注册表条目的 `status`/`supersededBy`(spec §5.4):
|
|
93
|
+
|
|
94
|
+
- **发现两个现存 type 其实是同一件事**(例如关卡三的可辨别性检查显示不出真实差异):
|
|
95
|
+
`agent-canvas-ctl relation-merge --from <旧type> --into <目标type> --gate-reasoning "<为什么判定等价>" [--evidence <relationId>,...]`
|
|
96
|
+
`from` 会被标记为 `deprecated`、`supersededBy` 指向 `into`;`into` 必须是当前存在的 type,
|
|
97
|
+
不存在会被拒绝。
|
|
98
|
+
- **发现一个现存 type 已经没有意义**(比如从未被真实使用、或语义已被更好的 type 完全覆盖):
|
|
99
|
+
`agent-canvas-ctl relation-deprecate --name <type> --gate-reasoning "<为什么判定过时>" [--evidence <relationId>,...]`
|
|
100
|
+
有代码消费点(`codePinned`,如 `spawns`/`implements`/`unspecified`)的 type 会被硬拒绝,
|
|
101
|
+
不要尝试废弃它们。
|
|
102
|
+
- **发现一个现存 type 的名字本身有误导性**:
|
|
103
|
+
`agent-canvas-ctl relation-rename --from <旧名字> --to <新名字> --gate-reasoning "<为什么需要改名>" [--evidence <relationId>,...]`
|
|
104
|
+
等价于"新建 `to` + 合并 `from`→`to`",`to` 必须已经存在(先用 `relation-promote`
|
|
105
|
+
建出新名字,再用这个命令把旧名字标记废弃)。
|
|
106
|
+
|
|
107
|
+
这三个动作不是每轮都会用到——待议区常年空转、四道关卡常年不通过是预期状态(spec §9.1),
|
|
108
|
+
这三个动作同样可能常年不触发。**不要为了"这次总得做点什么"而勉强套用它们**。
|
|
109
|
+
|
|
110
|
+
## 第五步:简短报告
|
|
111
|
+
|
|
112
|
+
列出:本轮待议区总数、本次新增数(是否达到唤起阈值)、审议的候选簇数、每簇的关卡结论
|
|
113
|
+
(通过/打回及原因)、执行了哪些升格(附 changelogId)、顺带执行了哪些废弃/合并/改名
|
|
114
|
+
(附 changelogId,没有就说明本轮没有)。
|
|
115
|
+
|
|
116
|
+
## 什么时候不要往下走
|
|
117
|
+
|
|
118
|
+
- 待议区新增 < `WAKE_N`:只报数量,不审议(见第一步)。
|
|
119
|
+
- 候选簇 < `PROMOTE_N` 或 < 2 个不同节点:打回在关卡二,不进关卡三。
|
|
120
|
+
- `agent-canvas-ctl` 命令不存在,或明显不是全局身份:整个 skill 不适用,跳过。
|
package/skills-index.json
CHANGED
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"writing": "写作工具(forge-doc + draw-diagram + manage-dir + migrate-spec)",
|
|
36
36
|
"design": "设计工具(scout-brand + build-style + sync-design)",
|
|
37
37
|
"mint": "Skill 生命周期工具(init-skill + publish-skill + archive-skill + dedup-skill + contribute-skill + fix-skill + runby-opencode + scout-philosophy + learn-skill)",
|
|
38
|
-
"devops": "项目运维工具(clean-git + release-project + sync-hotfix + sync-agent)"
|
|
38
|
+
"devops": "项目运维工具(clean-git + release-project + sync-hotfix + sync-agent)",
|
|
39
|
+
"agent-canvas": "Agent Canvas 内置 skill(agent-canvas-control — 节点与画布操控;describe-node/relate-node/capture-requirement/relation-review/close-node — 画布节点生命周期治理)"
|
|
39
40
|
},
|
|
40
41
|
"skills": [
|
|
41
42
|
{
|
|
@@ -344,6 +345,30 @@
|
|
|
344
345
|
"installScope": "project",
|
|
345
346
|
"contentHash": "27a9cb1a55b0036a",
|
|
346
347
|
"contentVersion": "0.2.0"
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
"path": "agent-canvas/agent-canvas-control",
|
|
351
|
+
"bundle": "agent-canvas"
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
"path": "agent-canvas/describe-node",
|
|
355
|
+
"bundle": "agent-canvas"
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
"path": "agent-canvas/relate-node",
|
|
359
|
+
"bundle": "agent-canvas"
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
"path": "agent-canvas/capture-requirement",
|
|
363
|
+
"bundle": "agent-canvas"
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
"path": "agent-canvas/relation-review",
|
|
367
|
+
"bundle": "agent-canvas"
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
"path": "agent-canvas/close-node",
|
|
371
|
+
"bundle": "agent-canvas"
|
|
347
372
|
}
|
|
348
373
|
],
|
|
349
374
|
"hooks": [
|