peaks-loop 4.0.0-beta.4 → 4.0.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +5 -0
- package/CHANGELOG.md +16 -0
- package/README-en.md +4 -0
- package/README.md +4 -0
- package/dist/src/cli/commands/core/skill-command.js +22 -0
- package/dist/src/cli/commands/skill-search-commands.d.ts +25 -0
- package/dist/src/cli/commands/skill-search-commands.js +68 -0
- package/dist/src/services/context/context-schema.d.ts +6 -6
- package/dist/src/services/skill/skill-search-service.d.ts +88 -0
- package/dist/src/services/skill/skill-search-service.js +228 -0
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/skills/peaks-solo/SKILL.md +197 -0
- package/skills/peaks-solo/references/fallback-tool-inventory.md +46 -0
- package/skills/peaks-solo/references/sediment-prompt-template.md +62 -0
- package/skills/peaks-solo/references/triage-decision-table.md +31 -0
|
@@ -34,6 +34,11 @@
|
|
|
34
34
|
"irreversible-action gates"
|
|
35
35
|
],
|
|
36
36
|
"skills": [
|
|
37
|
+
{
|
|
38
|
+
"name": "peaks-solo",
|
|
39
|
+
"path": "./skills/peaks-solo",
|
|
40
|
+
"description": "Dispatcher (分诊员) — natural-language front door for the Peaks-Loop skill family. Use when the user describes a task in NL and does not know which peaks-* skill fits. NOT for code/content/doctor/issue-fix-orchestrator/SOP work (use the specific skill)."
|
|
41
|
+
},
|
|
37
42
|
{
|
|
38
43
|
"name": "peaks-code",
|
|
39
44
|
"path": "./skills/peaks-code",
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## 4.0.0-beta.5 — 2026-07-08
|
|
6
|
+
|
|
7
|
+
### Added — peaks-solo dispatcher (分诊员)
|
|
8
|
+
|
|
9
|
+
- **`peaks-solo` skill** — `skills/peaks-solo/SKILL.md` + 3 references (triage / fallback / sediment). Natural-language front door for the Peaks-Loop skill family. Use when the user describes a task in NL and does not know which peaks-* skill fits. 0 breaking change: 3.x / 4.x `/peaks-code` / `/peaks-content` / `/peaks-doctor` etc. continue to work.
|
|
10
|
+
- **`peaks skill search` CLI** — `src/services/skill/skill-search-service.ts` + `src/cli/commands/skill-search-commands.ts`. Query / tag / domain filters; substring match; structured JSON output. Used by `peaks-solo` to find the right leaf skill. Available as a top-level primitive (not dispatcher-specific).
|
|
11
|
+
- **Sub-skills unchanged** — `peaks-code / peaks-content / peaks-doctor / peaks-issue-fix-orchestrator / peaks-sop / etc.` are NOT modified. peaks-solo sits alongside, not on top.
|
|
12
|
+
|
|
13
|
+
### Verification (4.0.0-beta.5)
|
|
14
|
+
|
|
15
|
+
- `peaks skill list` shows `peaks-solo` first
|
|
16
|
+
- `peaks skill search --query "code"` returns `peaks-code` with matchScore > 0
|
|
17
|
+
- `peaks skill search --query "xxxxxxxxxxxxx"` returns `[]` (no error)
|
|
18
|
+
- `pnpm vitest run` — full regression green; `peaks-code / peaks-content / peaks-doctor` tests unchanged
|
|
19
|
+
- `peaks code --help` / `peaks content --help` / `peaks doctor --help` — exit 0, behavior unchanged
|
|
20
|
+
|
|
5
21
|
## 4.0.0-beta.4 — 2026-07-08
|
|
6
22
|
|
|
7
23
|
Loop Engineering crystallization is now the product surface. The 4.0.0-beta.3 line shipped the framework; this release ships the post-run crystallization engine, the Darwin-style ratchet, the bundle share / desktop extension surface, and the karpathy-engineered red-line set that locks them together. Every durable-change entry is gated on a real, completed run; every evolution round is gated on an independent-context evaluation.
|
package/README-en.md
CHANGED
|
@@ -33,6 +33,10 @@ The product is governed by a four-layer asset model and a karpathy × darwin dua
|
|
|
33
33
|
|
|
34
34
|
Summon them, they handle the work. Stand them down when it's done. **No skipped steps. No half-finished hand-offs.** No new AI CLI to learn — it sits on top of Claude Code, Codex, or Copilot you already run.
|
|
35
35
|
|
|
36
|
+
### peaks-solo is the dispatcher (new in 4.0.0-beta.5)
|
|
37
|
+
|
|
38
|
+
If you **don't know which peaks-* skill to use**, just type `/peaks-solo` and describe your task in natural language. It triages for you: dispatches to a matching leaf, or self-plans with deep-search / WebSearch / Bash / Edit markdown if no leaf fits, then asks whether to sediment the result. `/peaks-code` / `/peaks-content` / `/peaks-doctor` etc. continue to work — **zero breaking changes**.
|
|
39
|
+
|
|
36
40
|
**Every gate has a hard exit.** Audit fails = stop. QA fails = stop. Any gate fails = full stop. You don't chase it, you don't patch it. You decide, it runs the next gate.
|
|
37
41
|
|
|
38
42
|
#### Gates aren't decoration — 5,439 tests passed · 19 skipped · 0 failed
|
package/README.md
CHANGED
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
|
|
34
34
|
召之即来,事完收队,**不跳步,不半截扔给你**。它不是新发明一个 AI CLI,而是架在你已有的 Claude Code / Codex / Copilot 之上,把它们都调成同一支团队,只对你这一个开发者的口味。
|
|
35
35
|
|
|
36
|
+
### peaks-solo 是分诊员(新增,4.0.0-beta.5)
|
|
37
|
+
|
|
38
|
+
如果**你不知道该用哪个 peaks-* 技能**,直接用 `/peaks-solo` 描述你的诉求就行。它会替你分诊:有合适的 leaf 就透明转交,没合适的就自己规划 + 跑(deep-search / WebSearch / Bash / Edit markdown),跑完回头问你要不要沉淀。`/peaks-code` / `/peaks-content` / `/peaks-doctor` 等老入口照常可用,**0 breaking**。
|
|
39
|
+
|
|
36
40
|
**每一步都有强退出条件**:审计不通过就停,QA 没过就停,任何一道门失败 = 全流程停。你不必追着它催、也不必替它补,它跑完一道再交你拍板,拍完再走下一道。
|
|
37
41
|
|
|
38
42
|
#### 门禁不是装饰 —— 5439 tests passed · 19 skipped · 0 failed
|
|
@@ -9,6 +9,15 @@ import { generateProjectContext } from '../../../services/memory/project-context
|
|
|
9
9
|
import { getSessionId, setSessionMeta } from '../../../services/session/session-manager.js';
|
|
10
10
|
import { fail, ok } from '../../../shared/result.js';
|
|
11
11
|
import { addJsonOption, getErrorMessage, printResult } from '../../cli-helpers.js';
|
|
12
|
+
// Slice S0 (4.0.0-beta.5 peaks-solo dispatcher release):
|
|
13
|
+
// `peaks skill search` is the CLI primitive that feeds the
|
|
14
|
+
// peaks-solo dispatcher (S1). Adding a single import + register call
|
|
15
|
+
// here keeps the change surgical and leaves all existing skill
|
|
16
|
+
// subcommands (list / doctor / sync / runbook / presence / heartbeat)
|
|
17
|
+
// untouched. See
|
|
18
|
+
// docs/superpowers/specs/2026-07-08-peaks-solo-dispatcher-design.md §3.2
|
|
19
|
+
// and the S0 plan under docs/superpowers/plans/.
|
|
20
|
+
import { registerSkillSearchCommand } from '../skill-search-commands.js';
|
|
12
21
|
export function registerSkillCommand(program, io) {
|
|
13
22
|
const skill = program.command('skill').description('Manage Peaks skills');
|
|
14
23
|
addJsonOption(skill.command('list').description('List skills derived from skills/*/SKILL.md')).action(async (options) => {
|
|
@@ -18,6 +27,15 @@ export function registerSkillCommand(program, io) {
|
|
|
18
27
|
}
|
|
19
28
|
else {
|
|
20
29
|
const sorted = [...skills].sort((a, b) => {
|
|
30
|
+
// Slice S0 (4.0.0-beta.5): peaks-solo is the dispatcher (front
|
|
31
|
+
// door) — list it FIRST so users discover the dispatcher before
|
|
32
|
+
// any specific leaf. Followed by peaks-sop (current default
|
|
33
|
+
// runbook showcase) and peaks-code (canonical code-domain
|
|
34
|
+
// orchestrator); everything else falls back to alphabetical.
|
|
35
|
+
if (a.name === 'peaks-solo')
|
|
36
|
+
return -1;
|
|
37
|
+
if (b.name === 'peaks-solo')
|
|
38
|
+
return 1;
|
|
21
39
|
if (a.name === 'peaks-sop')
|
|
22
40
|
return -1;
|
|
23
41
|
if (b.name === 'peaks-sop')
|
|
@@ -272,4 +290,8 @@ export function registerSkillCommand(program, io) {
|
|
|
272
290
|
const result = detectPresenceMarker({ project: projectRoot, latestAssistantMessage: message });
|
|
273
291
|
printResult(io, ok('skill.detect-marker-loss', result), options.json);
|
|
274
292
|
});
|
|
293
|
+
// Slice S0 — register `peaks skill search`. Sibling subcommand to
|
|
294
|
+
// list / runbook / presence; preserves the existing surface
|
|
295
|
+
// (HC-10 — 老入口保留).
|
|
296
|
+
registerSkillSearchCommand(program, io);
|
|
275
297
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peaks skill search — CLI primitive for S0 of the 4.0.0-beta.5
|
|
3
|
+
* peaks-solo dispatcher release.
|
|
4
|
+
*
|
|
5
|
+
* Spec: docs/superpowers/specs/2026-07-08-peaks-solo-dispatcher-design.md §3.2
|
|
6
|
+
* Plan: docs/superpowers/plans/2026-07-08-peaks-solo-dispatcher/s0-skill-search-cli.md
|
|
7
|
+
*
|
|
8
|
+
* Wired into the existing `peaks skill` group by
|
|
9
|
+
* `src/cli/commands/core/skill-command.ts` (one import + one call).
|
|
10
|
+
* The command ALWAYS emits a JSON array on stdout, even on the
|
|
11
|
+
* no-match case (which exits 0 with `[]` per plan §"Match rules (v1)").
|
|
12
|
+
*
|
|
13
|
+
* Exit codes (locked in plan §"API Contract"):
|
|
14
|
+
* 0 — success (may be empty array)
|
|
15
|
+
* 1 — invalid args (Zod validation failure)
|
|
16
|
+
* 2 — unexpected error (skill pool read fail, etc.)
|
|
17
|
+
*/
|
|
18
|
+
import type { Command } from 'commander';
|
|
19
|
+
import type { ProgramIO } from '../cli-helpers.js';
|
|
20
|
+
/**
|
|
21
|
+
* Validate the raw CLI flags, call the service, and emit a JSON
|
|
22
|
+
* envelope on stdout. Errors flow through stderr + non-zero exit
|
|
23
|
+
* codes per the plan §"API Contract" table.
|
|
24
|
+
*/
|
|
25
|
+
export declare function registerSkillSearchCommand(program: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { searchSkills, SkillSearchInputSchema } from '../../services/skill/skill-search-service.js';
|
|
2
|
+
import { getErrorMessage } from '../../shared/result.js';
|
|
3
|
+
/**
|
|
4
|
+
* Validate the raw CLI flags, call the service, and emit a JSON
|
|
5
|
+
* envelope on stdout. Errors flow through stderr + non-zero exit
|
|
6
|
+
* codes per the plan §"API Contract" table.
|
|
7
|
+
*/
|
|
8
|
+
export function registerSkillSearchCommand(program, io) {
|
|
9
|
+
// The parent `skill` command is created by registerSkillCommand in
|
|
10
|
+
// core/skill-command.ts. To keep the wiring self-contained we
|
|
11
|
+
// resolve the parent by name; this also lets tests register the
|
|
12
|
+
// subcommand in isolation.
|
|
13
|
+
const skill = program.commands.find((c) => c.name() === 'skill');
|
|
14
|
+
if (skill === undefined) {
|
|
15
|
+
// Defensive: the parent must be registered before us. Surface the
|
|
16
|
+
// error visibly — the dispatcher cannot triage without `peaks skill`
|
|
17
|
+
// existing, so the LLM-side caller needs to know.
|
|
18
|
+
throw new Error('peaks skill search: parent `skill` command not found. ' +
|
|
19
|
+
'Ensure registerSkillCommand is called before registerSkillSearchCommand.');
|
|
20
|
+
}
|
|
21
|
+
skill
|
|
22
|
+
.command('search')
|
|
23
|
+
.description('Search the in-tree skill pool by --query (substring on description + triggers) ' +
|
|
24
|
+
'and/or --tag (exact match on metadata.tags) and/or --domain (exact match). ' +
|
|
25
|
+
'Outputs a JSON array (always, even on no-match) to stdout. ' +
|
|
26
|
+
'See docs/superpowers/specs/2026-07-08-peaks-solo-dispatcher-design.md §3.2.')
|
|
27
|
+
.option('-q, --query <text>', 'case-insensitive substring matched against skill description + triggers')
|
|
28
|
+
.option('-t, --tag <tag>', 'exact match on metadata.tags (e.g. "orchestrator", "loop-engineering")')
|
|
29
|
+
.option('-d, --domain <domain>', 'exact match on metadata.domain (locked enum: code | content | doctor | research | triage | sop | audit | final-review | resume | status | test | ide | slice-decompose | issue-fix-orchestrator | perf-audit | security-audit | reviewer)')
|
|
30
|
+
.option('-l, --limit <n>', 'max results returned (1..100, default 20)', (v) => Number.parseInt(v, 10))
|
|
31
|
+
.action(async (options) => {
|
|
32
|
+
// 1) Coerce + validate input via the service's Zod schema.
|
|
33
|
+
// The schema's refine rejects empty input — that is the
|
|
34
|
+
// "all-empty → error" contract the spec mandates.
|
|
35
|
+
const raw = {};
|
|
36
|
+
if (options.query !== undefined)
|
|
37
|
+
raw['query'] = options.query;
|
|
38
|
+
if (options.tag !== undefined)
|
|
39
|
+
raw['tag'] = options.tag;
|
|
40
|
+
if (options.domain !== undefined)
|
|
41
|
+
raw['domain'] = options.domain;
|
|
42
|
+
if (options.limit !== undefined)
|
|
43
|
+
raw['limit'] = options.limit;
|
|
44
|
+
const parsed = SkillSearchInputSchema.safeParse(raw);
|
|
45
|
+
if (!parsed.success) {
|
|
46
|
+
// Exit code 1 = invalid args. Emit a human-readable message
|
|
47
|
+
// on stderr so the LLM-side caller can act on it; do NOT
|
|
48
|
+
// pollute stdout (which downstream tools parse as JSON).
|
|
49
|
+
for (const issue of parsed.error.issues) {
|
|
50
|
+
io.stderr(`${issue.path.join('.') || '<root>'}: ${issue.message}`);
|
|
51
|
+
}
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// 2) Call the service. Any unexpected error (skill pool read
|
|
56
|
+
// fail, etc.) maps to exit code 2.
|
|
57
|
+
try {
|
|
58
|
+
const results = await searchSkills(parsed.data);
|
|
59
|
+
// Always emit a JSON array on stdout. The plan locks this
|
|
60
|
+
// contract: even no-match returns `[]`, never `null`.
|
|
61
|
+
io.stdout(JSON.stringify(results));
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
io.stderr(`skill.search failed: ${getErrorMessage(error)}`);
|
|
65
|
+
process.exitCode = 2;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -223,39 +223,39 @@ export declare const ContextJsonSchema: z.ZodObject<{
|
|
|
223
223
|
tags: z.ZodArray<z.ZodString, "many">;
|
|
224
224
|
}, "strip", z.ZodTypeAny, {
|
|
225
225
|
kind: "git" | "code" | "doc" | "memory";
|
|
226
|
+
tags: string[];
|
|
226
227
|
id: string;
|
|
227
228
|
blastRadius: string[];
|
|
228
229
|
conflictScore: number;
|
|
229
230
|
timeDecayScore: number;
|
|
230
|
-
tags: string[];
|
|
231
231
|
version?: string | undefined;
|
|
232
232
|
}, {
|
|
233
233
|
kind: "git" | "code" | "doc" | "memory";
|
|
234
|
+
tags: string[];
|
|
234
235
|
id: string;
|
|
235
236
|
blastRadius: string[];
|
|
236
237
|
conflictScore: number;
|
|
237
238
|
timeDecayScore: number;
|
|
238
|
-
tags: string[];
|
|
239
239
|
version?: string | undefined;
|
|
240
240
|
}>, "many">;
|
|
241
241
|
}, "strip", z.ZodTypeAny, {
|
|
242
242
|
metadata: {
|
|
243
243
|
kind: "git" | "code" | "doc" | "memory";
|
|
244
|
+
tags: string[];
|
|
244
245
|
id: string;
|
|
245
246
|
blastRadius: string[];
|
|
246
247
|
conflictScore: number;
|
|
247
248
|
timeDecayScore: number;
|
|
248
|
-
tags: string[];
|
|
249
249
|
version?: string | undefined;
|
|
250
250
|
}[];
|
|
251
251
|
}, {
|
|
252
252
|
metadata: {
|
|
253
253
|
kind: "git" | "code" | "doc" | "memory";
|
|
254
|
+
tags: string[];
|
|
254
255
|
id: string;
|
|
255
256
|
blastRadius: string[];
|
|
256
257
|
conflictScore: number;
|
|
257
258
|
timeDecayScore: number;
|
|
258
|
-
tags: string[];
|
|
259
259
|
version?: string | undefined;
|
|
260
260
|
}[];
|
|
261
261
|
}>;
|
|
@@ -330,11 +330,11 @@ export declare const ContextJsonSchema: z.ZodObject<{
|
|
|
330
330
|
tokenizer: {
|
|
331
331
|
metadata: {
|
|
332
332
|
kind: "git" | "code" | "doc" | "memory";
|
|
333
|
+
tags: string[];
|
|
333
334
|
id: string;
|
|
334
335
|
blastRadius: string[];
|
|
335
336
|
conflictScore: number;
|
|
336
337
|
timeDecayScore: number;
|
|
337
|
-
tags: string[];
|
|
338
338
|
version?: string | undefined;
|
|
339
339
|
}[];
|
|
340
340
|
};
|
|
@@ -397,11 +397,11 @@ export declare const ContextJsonSchema: z.ZodObject<{
|
|
|
397
397
|
tokenizer: {
|
|
398
398
|
metadata: {
|
|
399
399
|
kind: "git" | "code" | "doc" | "memory";
|
|
400
|
+
tags: string[];
|
|
400
401
|
id: string;
|
|
401
402
|
blastRadius: string[];
|
|
402
403
|
conflictScore: number;
|
|
403
404
|
timeDecayScore: number;
|
|
404
|
-
tags: string[];
|
|
405
405
|
version?: string | undefined;
|
|
406
406
|
}[];
|
|
407
407
|
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peaks skill search — service-layer primitive for S0 of the
|
|
3
|
+
* 4.0.0-beta.5 peaks-solo dispatcher release.
|
|
4
|
+
*
|
|
5
|
+
* Spec: docs/superpowers/specs/2026-07-08-peaks-solo-dispatcher-design.md §3.2
|
|
6
|
+
* Plan: docs/superpowers/plans/2026-07-08-peaks-solo-dispatcher/s0-skill-search-cli.md
|
|
7
|
+
*
|
|
8
|
+
* This service is the single source of truth for skill pool retrieval
|
|
9
|
+
* and is reused by the `peaks skill search` CLI. It is intentionally
|
|
10
|
+
* pure (synchronous frontmatter parse + substring scoring) so the
|
|
11
|
+
* dispatcher (`peaks-solo`, S1) can call it without paying for any
|
|
12
|
+
* async I/O during the triage decision flow.
|
|
13
|
+
*
|
|
14
|
+
* v1 scope (locked, see plan §"Match rules (v1)"):
|
|
15
|
+
* - --query: case-insensitive substring match on description + triggers
|
|
16
|
+
* - --tag: case-insensitive exact match on `metadata.tags`
|
|
17
|
+
* - --domain: exact match on `metadata.domain` (locked enum)
|
|
18
|
+
* - All three compose with AND; at least one filter is required
|
|
19
|
+
* - No match → `[]` (not error, not null)
|
|
20
|
+
* - --limit truncates to the top N by matchScore desc
|
|
21
|
+
*
|
|
22
|
+
* Out of v1 scope (deferred, see plan §"Goal"): fuzzy / FTS5 / semantic
|
|
23
|
+
* matching, ML-based ranking, pagination, i18n, caching.
|
|
24
|
+
*/
|
|
25
|
+
import { z } from 'zod';
|
|
26
|
+
import { skillsDir } from '../../shared/paths.js';
|
|
27
|
+
export declare const SkillSearchInputSchema: z.ZodEffects<z.ZodObject<{
|
|
28
|
+
query: z.ZodOptional<z.ZodString>;
|
|
29
|
+
tag: z.ZodOptional<z.ZodString>;
|
|
30
|
+
domain: z.ZodOptional<z.ZodEnum<["code", "content", "doctor", "research", "triage", "sop", "audit", "final-review", "resume", "status", "test", "ide", "slice-decompose", "issue-fix-orchestrator", "perf-audit", "security-audit", "reviewer"]>>;
|
|
31
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
32
|
+
}, "strip", z.ZodTypeAny, {
|
|
33
|
+
limit: number;
|
|
34
|
+
query?: string | undefined;
|
|
35
|
+
tag?: string | undefined;
|
|
36
|
+
domain?: "code" | "status" | "content" | "test" | "doctor" | "resume" | "ide" | "research" | "triage" | "sop" | "audit" | "final-review" | "slice-decompose" | "issue-fix-orchestrator" | "perf-audit" | "security-audit" | "reviewer" | undefined;
|
|
37
|
+
}, {
|
|
38
|
+
limit?: number | undefined;
|
|
39
|
+
query?: string | undefined;
|
|
40
|
+
tag?: string | undefined;
|
|
41
|
+
domain?: "code" | "status" | "content" | "test" | "doctor" | "resume" | "ide" | "research" | "triage" | "sop" | "audit" | "final-review" | "slice-decompose" | "issue-fix-orchestrator" | "perf-audit" | "security-audit" | "reviewer" | undefined;
|
|
42
|
+
}>, {
|
|
43
|
+
limit: number;
|
|
44
|
+
query?: string | undefined;
|
|
45
|
+
tag?: string | undefined;
|
|
46
|
+
domain?: "code" | "status" | "content" | "test" | "doctor" | "resume" | "ide" | "research" | "triage" | "sop" | "audit" | "final-review" | "slice-decompose" | "issue-fix-orchestrator" | "perf-audit" | "security-audit" | "reviewer" | undefined;
|
|
47
|
+
}, {
|
|
48
|
+
limit?: number | undefined;
|
|
49
|
+
query?: string | undefined;
|
|
50
|
+
tag?: string | undefined;
|
|
51
|
+
domain?: "code" | "status" | "content" | "test" | "doctor" | "resume" | "ide" | "research" | "triage" | "sop" | "audit" | "final-review" | "slice-decompose" | "issue-fix-orchestrator" | "perf-audit" | "security-audit" | "reviewer" | undefined;
|
|
52
|
+
}>;
|
|
53
|
+
export type SkillSearchInput = z.input<typeof SkillSearchInputSchema>;
|
|
54
|
+
export declare const SkillSearchResultSchema: z.ZodObject<{
|
|
55
|
+
name: z.ZodString;
|
|
56
|
+
description: z.ZodString;
|
|
57
|
+
triggers: z.ZodArray<z.ZodString, "many">;
|
|
58
|
+
tags: z.ZodArray<z.ZodString, "many">;
|
|
59
|
+
domain: z.ZodString;
|
|
60
|
+
matchScore: z.ZodNumber;
|
|
61
|
+
}, "strip", z.ZodTypeAny, {
|
|
62
|
+
name: string;
|
|
63
|
+
description: string;
|
|
64
|
+
domain: string;
|
|
65
|
+
triggers: string[];
|
|
66
|
+
tags: string[];
|
|
67
|
+
matchScore: number;
|
|
68
|
+
}, {
|
|
69
|
+
name: string;
|
|
70
|
+
description: string;
|
|
71
|
+
domain: string;
|
|
72
|
+
triggers: string[];
|
|
73
|
+
tags: string[];
|
|
74
|
+
matchScore: number;
|
|
75
|
+
}>;
|
|
76
|
+
export type SkillSearchResult = z.infer<typeof SkillSearchResultSchema>;
|
|
77
|
+
/**
|
|
78
|
+
* Search the in-tree skill pool. Validates `input` against
|
|
79
|
+
* `SkillSearchInputSchema`; the schema's refine rejects empty input
|
|
80
|
+
* with a ZodError, which is the contract test U-7 / I-3 rely on.
|
|
81
|
+
*
|
|
82
|
+
* @param input One of `query | tag | domain` is required.
|
|
83
|
+
* All provided filters compose with AND.
|
|
84
|
+
* @returns Matching skills sorted by matchScore desc, name asc.
|
|
85
|
+
* Empty array (not error, not null) when nothing matches.
|
|
86
|
+
*/
|
|
87
|
+
export declare function searchSkills(rawInput: SkillSearchInput): Promise<SkillSearchResult[]>;
|
|
88
|
+
export { skillsDir as __skillsDirForTests };
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peaks skill search — service-layer primitive for S0 of the
|
|
3
|
+
* 4.0.0-beta.5 peaks-solo dispatcher release.
|
|
4
|
+
*
|
|
5
|
+
* Spec: docs/superpowers/specs/2026-07-08-peaks-solo-dispatcher-design.md §3.2
|
|
6
|
+
* Plan: docs/superpowers/plans/2026-07-08-peaks-solo-dispatcher/s0-skill-search-cli.md
|
|
7
|
+
*
|
|
8
|
+
* This service is the single source of truth for skill pool retrieval
|
|
9
|
+
* and is reused by the `peaks skill search` CLI. It is intentionally
|
|
10
|
+
* pure (synchronous frontmatter parse + substring scoring) so the
|
|
11
|
+
* dispatcher (`peaks-solo`, S1) can call it without paying for any
|
|
12
|
+
* async I/O during the triage decision flow.
|
|
13
|
+
*
|
|
14
|
+
* v1 scope (locked, see plan §"Match rules (v1)"):
|
|
15
|
+
* - --query: case-insensitive substring match on description + triggers
|
|
16
|
+
* - --tag: case-insensitive exact match on `metadata.tags`
|
|
17
|
+
* - --domain: exact match on `metadata.domain` (locked enum)
|
|
18
|
+
* - All three compose with AND; at least one filter is required
|
|
19
|
+
* - No match → `[]` (not error, not null)
|
|
20
|
+
* - --limit truncates to the top N by matchScore desc
|
|
21
|
+
*
|
|
22
|
+
* Out of v1 scope (deferred, see plan §"Goal"): fuzzy / FTS5 / semantic
|
|
23
|
+
* matching, ML-based ranking, pagination, i18n, caching.
|
|
24
|
+
*/
|
|
25
|
+
import { z } from 'zod';
|
|
26
|
+
import { listSkills } from '../skills/skill-registry.js';
|
|
27
|
+
import { parseFrontmatter } from '../../shared/frontmatter.js';
|
|
28
|
+
import { readText } from '../../shared/fs.js';
|
|
29
|
+
import { skillsDir } from '../../shared/paths.js';
|
|
30
|
+
const SKILL_DOMAINS = [
|
|
31
|
+
'code',
|
|
32
|
+
'content',
|
|
33
|
+
'doctor',
|
|
34
|
+
'research',
|
|
35
|
+
'triage',
|
|
36
|
+
'sop',
|
|
37
|
+
'audit',
|
|
38
|
+
'final-review',
|
|
39
|
+
'resume',
|
|
40
|
+
'status',
|
|
41
|
+
'test',
|
|
42
|
+
'ide',
|
|
43
|
+
'slice-decompose',
|
|
44
|
+
'issue-fix-orchestrator',
|
|
45
|
+
'perf-audit',
|
|
46
|
+
'security-audit',
|
|
47
|
+
'reviewer'
|
|
48
|
+
];
|
|
49
|
+
export const SkillSearchInputSchema = z
|
|
50
|
+
.object({
|
|
51
|
+
query: z.string().min(1).max(500).optional(),
|
|
52
|
+
tag: z.string().min(1).max(100).optional(),
|
|
53
|
+
domain: z.enum(SKILL_DOMAINS).optional(),
|
|
54
|
+
limit: z.number().int().min(1).max(100).optional().default(20)
|
|
55
|
+
})
|
|
56
|
+
.refine((v) => v.query !== undefined || v.tag !== undefined || v.domain !== undefined, { message: 'At least one of --query / --tag / --domain is required' });
|
|
57
|
+
export const SkillSearchResultSchema = z.object({
|
|
58
|
+
name: z.string(),
|
|
59
|
+
description: z.string(),
|
|
60
|
+
triggers: z.array(z.string()),
|
|
61
|
+
tags: z.array(z.string()),
|
|
62
|
+
domain: z.string(),
|
|
63
|
+
matchScore: z.number().min(0).max(1)
|
|
64
|
+
});
|
|
65
|
+
/**
|
|
66
|
+
* Extract trigger phrases from a skill description. The convention is
|
|
67
|
+
* `Triggers on \`/x\`, "y", "z".` at the end of the description
|
|
68
|
+
* (or anywhere within). v1 parses quoted/back-tick tokens after the
|
|
69
|
+
* "Triggers on" marker. When the marker is missing, the trigger set
|
|
70
|
+
* is empty (description-only matching still works).
|
|
71
|
+
*/
|
|
72
|
+
function extractTriggers(description) {
|
|
73
|
+
const lower = description.toLowerCase();
|
|
74
|
+
const marker = 'triggers on';
|
|
75
|
+
const idx = lower.indexOf(marker);
|
|
76
|
+
if (idx === -1)
|
|
77
|
+
return [];
|
|
78
|
+
const after = description.slice(idx + marker.length);
|
|
79
|
+
// Stop at the first period that ends the trigger clause.
|
|
80
|
+
// Heuristic: take everything up to the next `.` that is followed by
|
|
81
|
+
// whitespace + capital letter, or end-of-string.
|
|
82
|
+
const stopMatch = /\.\s+[A-Z]|\.\s*$/.exec(after);
|
|
83
|
+
const clause = stopMatch ? after.slice(0, stopMatch.index + 1) : after;
|
|
84
|
+
// Extract every quoted/back-tick token.
|
|
85
|
+
const tokenRegex = /[`"']([^`"']+)[`"']/g;
|
|
86
|
+
const out = [];
|
|
87
|
+
let m;
|
|
88
|
+
while ((m = tokenRegex.exec(clause)) !== null) {
|
|
89
|
+
if (m[1] !== undefined)
|
|
90
|
+
out.push(m[1]);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Parse metadata tags / domain from the raw SKILL.md frontmatter.
|
|
96
|
+
* `parseFrontmatter()` flattens nested YAML mapping into dotted keys,
|
|
97
|
+
* so `metadata.tags: a, b` becomes a single string we split on `, `.
|
|
98
|
+
* The existing `skill-registry` already calls `parseFrontmatter` but
|
|
99
|
+
* does not surface the metadata block, so we re-parse the file.
|
|
100
|
+
*
|
|
101
|
+
* R8 mitigation: if the file is missing, malformed, or unreadable we
|
|
102
|
+
* return empty tags + domain so the search itself can still proceed
|
|
103
|
+
* with description-only matching. The malformed-file warning is
|
|
104
|
+
* already emitted by the registry layer (`loadSkillRegistry`'s
|
|
105
|
+
* `failures[]` channel); this function is best-effort metadata.
|
|
106
|
+
*/
|
|
107
|
+
async function parseMetadata(skillPath) {
|
|
108
|
+
try {
|
|
109
|
+
const raw = await readText(skillPath);
|
|
110
|
+
const fm = parseFrontmatter(raw);
|
|
111
|
+
const tagsRaw = fm['metadata.tags'] ?? '';
|
|
112
|
+
const tags = tagsRaw
|
|
113
|
+
.split(',')
|
|
114
|
+
.map((t) => t.trim())
|
|
115
|
+
.filter((t) => t.length > 0);
|
|
116
|
+
const domain = (fm['metadata.domain'] ?? '').trim();
|
|
117
|
+
return { tags, domain };
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return { tags: [], domain: '' };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function loadEnrichedSkills() {
|
|
124
|
+
const skills = await listSkills();
|
|
125
|
+
return Promise.all(skills.map(async (s) => {
|
|
126
|
+
const meta = await parseMetadata(s.skillPath);
|
|
127
|
+
return {
|
|
128
|
+
name: s.name,
|
|
129
|
+
description: s.description,
|
|
130
|
+
triggers: extractTriggers(s.description),
|
|
131
|
+
tags: meta.tags,
|
|
132
|
+
domain: meta.domain,
|
|
133
|
+
skillPath: s.skillPath
|
|
134
|
+
};
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Substring match score for a single (skill, query) pair.
|
|
139
|
+
*
|
|
140
|
+
* matchScore = 0.5 * (query_hits_in_description / description_length_words)
|
|
141
|
+
* + 0.5 * (query_hits_in_triggers / triggers_count)
|
|
142
|
+
* clamped to [0, 1]
|
|
143
|
+
*
|
|
144
|
+
* "hits" = number of occurrences of the query substring (lowercased)
|
|
145
|
+
* in the lowercased target. Length is normalized as word count (so a
|
|
146
|
+
* 1-word description that matches returns the full 0.5 contribution).
|
|
147
|
+
*/
|
|
148
|
+
function scoreQuery(query, description, triggers) {
|
|
149
|
+
const q = query.toLowerCase();
|
|
150
|
+
const desc = description.toLowerCase();
|
|
151
|
+
const descHits = countOccurrences(desc, q);
|
|
152
|
+
const descWords = Math.max(1, desc.split(/\s+/).filter((w) => w.length > 0).length);
|
|
153
|
+
const descContribution = Math.min(1, descHits / descWords);
|
|
154
|
+
const triggersJoined = triggers.map((t) => t.toLowerCase()).join(' | ');
|
|
155
|
+
const trigHits = countOccurrences(triggersJoined, q);
|
|
156
|
+
const trigCount = Math.max(1, triggers.length);
|
|
157
|
+
const trigContribution = Math.min(1, trigHits / trigCount);
|
|
158
|
+
const raw = 0.5 * descContribution + 0.5 * trigContribution;
|
|
159
|
+
// Clamp to [0, 1] and round to 4 decimals to keep envelope tidy.
|
|
160
|
+
return Math.round(Math.max(0, Math.min(1, raw)) * 10_000) / 10_000;
|
|
161
|
+
}
|
|
162
|
+
function countOccurrences(haystack, needle) {
|
|
163
|
+
if (needle.length === 0)
|
|
164
|
+
return 0;
|
|
165
|
+
let count = 0;
|
|
166
|
+
let pos = 0;
|
|
167
|
+
while (pos <= haystack.length) {
|
|
168
|
+
const next = haystack.indexOf(needle, pos);
|
|
169
|
+
if (next === -1)
|
|
170
|
+
break;
|
|
171
|
+
count += 1;
|
|
172
|
+
pos = next + needle.length;
|
|
173
|
+
}
|
|
174
|
+
return count;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Search the in-tree skill pool. Validates `input` against
|
|
178
|
+
* `SkillSearchInputSchema`; the schema's refine rejects empty input
|
|
179
|
+
* with a ZodError, which is the contract test U-7 / I-3 rely on.
|
|
180
|
+
*
|
|
181
|
+
* @param input One of `query | tag | domain` is required.
|
|
182
|
+
* All provided filters compose with AND.
|
|
183
|
+
* @returns Matching skills sorted by matchScore desc, name asc.
|
|
184
|
+
* Empty array (not error, not null) when nothing matches.
|
|
185
|
+
*/
|
|
186
|
+
export async function searchSkills(rawInput) {
|
|
187
|
+
const input = SkillSearchInputSchema.parse(rawInput);
|
|
188
|
+
const skills = await loadEnrichedSkills();
|
|
189
|
+
const filtered = skills.filter((skill) => {
|
|
190
|
+
if (input.tag !== undefined) {
|
|
191
|
+
const wanted = input.tag.toLowerCase();
|
|
192
|
+
const has = skill.tags.some((t) => t.toLowerCase() === wanted);
|
|
193
|
+
if (!has)
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
if (input.domain !== undefined) {
|
|
197
|
+
if (skill.domain.toLowerCase() !== input.domain.toLowerCase())
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
if (input.query !== undefined) {
|
|
201
|
+
const q = input.query.toLowerCase();
|
|
202
|
+
const inDesc = skill.description.toLowerCase().includes(q);
|
|
203
|
+
const inTrig = skill.triggers.some((t) => t.toLowerCase().includes(q));
|
|
204
|
+
if (!inDesc && !inTrig)
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
return true;
|
|
208
|
+
});
|
|
209
|
+
const scored = filtered.map((skill) => ({
|
|
210
|
+
name: skill.name,
|
|
211
|
+
description: skill.description,
|
|
212
|
+
triggers: skill.triggers,
|
|
213
|
+
tags: skill.tags,
|
|
214
|
+
domain: skill.domain,
|
|
215
|
+
matchScore: input.query !== undefined
|
|
216
|
+
? scoreQuery(input.query, skill.description, skill.triggers)
|
|
217
|
+
: 1
|
|
218
|
+
}));
|
|
219
|
+
scored.sort((a, b) => {
|
|
220
|
+
if (b.matchScore !== a.matchScore)
|
|
221
|
+
return b.matchScore - a.matchScore;
|
|
222
|
+
return a.name.localeCompare(b.name);
|
|
223
|
+
});
|
|
224
|
+
return scored.slice(0, input.limit);
|
|
225
|
+
}
|
|
226
|
+
// Re-export `skillsDir` only for tests that want to assert on-disk I/O
|
|
227
|
+
// behavior end-to-end. Not part of the public CLI surface.
|
|
228
|
+
export { skillsDir as __skillsDirForTests };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "4.0.0-beta.
|
|
1
|
+
export declare const CLI_VERSION = "4.0.0-beta.5";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "4.0.0-beta.
|
|
1
|
+
export const CLI_VERSION = "4.0.0-beta.5";
|
package/package.json
CHANGED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: peaks-solo
|
|
3
|
+
description: |
|
|
4
|
+
Dispatcher (分诊员) for the Peaks-Loop skill family. Use when the user describes a task in natural language and does NOT know which peaks-* skill fits. peaks-solo reads the live skill pool via `peaks skill search`, dispatches to a matching leaf (peaks-code / peaks-content / peaks-doctor / peaks-issue-fix-orchestrator / etc.) or falls back to self-planned execution (deep-search / WebSearch / Bash / Edit markdown) if no leaf matches, and then asks the user whether to sediment the result.
|
|
5
|
+
|
|
6
|
+
Triggers: 自然语言描述诉求且无明确 peaks-* skill 选择 / "帮我处理这个" / "我不知道该用哪个" / "随便都行".
|
|
7
|
+
|
|
8
|
+
NOT for: code-specific work (use /peaks-code) / content-specific work (use /peaks-content) / project health check (use /peaks-doctor) / issue sweep (use /peaks-issue-fix-orchestrator) / SOP authoring (use /peaks-sop).
|
|
9
|
+
metadata:
|
|
10
|
+
type: dispatcher
|
|
11
|
+
domain: triage
|
|
12
|
+
visibility: public
|
|
13
|
+
supersedes: peaks-solo (eradicated 2026-07-05, revived 2026-07-08)
|
|
14
|
+
red_lines: [RL-1, RL-8, HC-7, HC-8, HC-9, HC-10, HC-11]
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# peaks-solo — Dispatcher (分诊员)
|
|
18
|
+
|
|
19
|
+
> peaks-solo 是 Peaks-Loop 的 dispatcher,不是 orchestrator。读取技能池 → 分诊 → 转交 leaf 或自规划 → 回头问沉淀。**不持有 implementation surface**。
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 1. 角色定义 (dispatcher ≠ orchestrator)
|
|
24
|
+
|
|
25
|
+
**peaks-solo 是一条 dispatcher skill**,跟 `peaks-code` 这种 orchestrator 平行存在,但更薄一层:
|
|
26
|
+
|
|
27
|
+
| 角色 | 工作 | 是否持有 implementation surface |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| **dispatcher** (peaks-solo,本 skill) | 看用户 NL → 查 skill 池 → 转交 / 自规划 → 问沉淀 | **不持有**(只调度,不实现) |
|
|
30
|
+
| **orchestrator** (peaks-code / peaks-content / peaks-doctor) | 看用户 NL → 走 11 步 runbook → 直接调 prd/rd/qa/ui/sc/txt 蜂群 → 沉淀 | **持有**(通过子蜂群间接持有) |
|
|
31
|
+
|
|
32
|
+
**核心区分(HC-11 锁死):** peaks-solo 能看、能查、能转交、能跑通用工具(deep-search / WebSearch / Bash / Edit markdown)、能提议沉淀;**不能**写代码、**不能**写 PRD、**不能**跑 vitest、**不能**改 Loop Engineering 资产(那是用户主权)。
|
|
33
|
+
|
|
34
|
+
**与 peaks-* leaf 的关系:** peaks-solo 是新加的第三条入口,不替代任何 leaf。3.x / 4.x 老用户继续 `/peaks-code` / `/peaks-content` / `/peaks-doctor` 等,**老入口 0 改动**(HC-10)。
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 2. 触发条件
|
|
39
|
+
|
|
40
|
+
peaks-solo 由 LLM 在三种匹配类之一触发,**不依赖**任何固定的 slash 命令前缀:
|
|
41
|
+
|
|
42
|
+
### 2.1 Source-trace 触发
|
|
43
|
+
|
|
44
|
+
LLM 自身启动循环(如 Claude Code 启动新会话、resume 检测、auto-decide 路径)时,**没有**其它 skill 已被 dispatch,且用户描述诉求 → 走 peaks-solo 分诊。
|
|
45
|
+
|
|
46
|
+
### 2.2 Trigger-phrase 触发(主路径)
|
|
47
|
+
|
|
48
|
+
用户自然语言描述诉求 + 满足下列任一短语:
|
|
49
|
+
|
|
50
|
+
- 自然语言描述诉求且无明确 peaks-* skill 选择(主触发)
|
|
51
|
+
- "帮我处理这个" / "帮我看看"
|
|
52
|
+
- "我不知道该用哪个"
|
|
53
|
+
- "随便都行" / "你帮我决定"
|
|
54
|
+
- "我有个事需要处理" / "我想做点东西"
|
|
55
|
+
|
|
56
|
+
**反向:** 用户已明确指定 `/peaks-code` / `/peaks-content` / `/peaks-doctor` 等具体 skill → 不进 peaks-solo,直接 dispatch 到该 leaf(HC-10 老入口保留)。
|
|
57
|
+
|
|
58
|
+
### 2.3 LLM-judge 触发
|
|
59
|
+
|
|
60
|
+
用户 NL 表述是开放性、跨域、或者 LLM 无法判定属于哪个 peaks-* 域时(例如"帮我看下 GitHub trending top 10"),LLM 自己判断"这事没有专属 leaf",走 peaks-solo 兜底。
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 3. Triage 决策流
|
|
65
|
+
|
|
66
|
+
peaks-solo 的 triage 流严格按以下伪代码执行:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
1. user NL → peaks-solo 触发
|
|
70
|
+
2. peaks-solo 跑 peaks skill search --query "<NL>" → 拿到 candidates []
|
|
71
|
+
3. if candidates.length == 0:
|
|
72
|
+
→ 走自规划兜底(§4)
|
|
73
|
+
4. if candidates.length == 1:
|
|
74
|
+
→ 直接 Skill tool 调起该 skill(转交,无感)
|
|
75
|
+
5. if candidates.length >= 2:
|
|
76
|
+
→ 走 AskUserQuestion 多选("NL 描述匹配到 N 个 peaks-* skill,请选一个:")
|
|
77
|
+
→ 用户选 → Skill tool 调起
|
|
78
|
+
→ 用户选 "(e) 都不对" → 自规划兜底(§4)
|
|
79
|
+
6. leaf 跑完 → leaf 内部自己处理 sediment(不重复问;HC-9)
|
|
80
|
+
7. 自规划跑完 → 走 §5 沉淀提议
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 3.1 多候选 AskUserQuestion 模板(HC-9 锁死)
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
question: "这个诉求匹配到以下 peaks-* skill,请选一个:"
|
|
87
|
+
options:
|
|
88
|
+
- (a) <name + 一句话功能 + 适用场景>
|
|
89
|
+
- (b) <name + 一句话功能 + 适用场景>
|
|
90
|
+
- ...
|
|
91
|
+
- (e) 都不对,我自己跑
|
|
92
|
+
multiSelect: false
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**关键约束:** 必须是 AskUserQuestion,**不**允许让用户自由文本输入 skill 名(HC-9 Human-NL-Choice-Only)。
|
|
96
|
+
|
|
97
|
+
### 3.2 用户主动说"我自己来" / "不用分诊"
|
|
98
|
+
|
|
99
|
+
LLM **bail out**(返回控制权给用户,不调任何 leaf,不进自规划兜底)。
|
|
100
|
+
|
|
101
|
+
### 3.3 关键词 → leaf skill 快速映射
|
|
102
|
+
|
|
103
|
+
完整 ≥ 10 行关键词表见 `references/triage-decision-table.md`。下面只列最常见的 5 类:
|
|
104
|
+
|
|
105
|
+
| keyword | → leaf skill |
|
|
106
|
+
|---|---|
|
|
107
|
+
| code / 改 bug / 全流程 / 端到端 | `/peaks-code` |
|
|
108
|
+
| 文档 / article / 内容 / 写稿 | `/peaks-content` |
|
|
109
|
+
| 健康 / 体检 / audit 报告 | `/peaks-doctor` |
|
|
110
|
+
| issue / 批量修 / sweep | `/peaks-issue-fix-orchestrator` |
|
|
111
|
+
| SOP / 流程 / 工作流 | `/peaks-sop` |
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 4. 自规划兜底
|
|
116
|
+
|
|
117
|
+
当 `peaks skill search` 返回 0 个候选,或用户选"(e) 都不对",LLM 走自规划:用通用工具(deep-search / WebSearch / Bash / Edit markdown)完成用户诉求。
|
|
118
|
+
|
|
119
|
+
### 4.1 允许工具清单
|
|
120
|
+
|
|
121
|
+
| 工具 | 适用场景 | 限制 |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| `deep-search` skill | 信息查询 / 研究类(若已装) | 不主动 install |
|
|
124
|
+
| `WebSearch` / `WebFetch` | 实时性高的查询(超过训练数据) | 内置工具 |
|
|
125
|
+
| `Bash` | 数值计算 / 系统命令 / git 操作 | **不写 src/** 业务代码(那是 peaks-code 域) |
|
|
126
|
+
| `Edit` / `Write` | 改 markdown / 改 yaml / 改 `.peaks/memory/` 文件 | **不改 src/** 业务代码;**不改 `skills/peaks-{code,content,doctor,...}/`** 任何 leaf skill 文件 |
|
|
127
|
+
| `peaks memory extract` | 跑完提议沉淀 | 走现成 CLI,不重写 |
|
|
128
|
+
|
|
129
|
+
完整 allowed + blocked 清单见 `references/fallback-tool-inventory.md`。
|
|
130
|
+
|
|
131
|
+
### 4.2 自规划成功后的下一步
|
|
132
|
+
|
|
133
|
+
自规划跑完 **必须** 走 §5 沉淀提议(HC-9 Human-NL-Choice-Only);不允许静默丢弃结果。
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 5. 沉淀提议
|
|
138
|
+
|
|
139
|
+
自规划兜底跑完(leaf 转交跑完则由 leaf 内部 Step 11 处理,不重复问)→ peaks-solo 用 AskUserQuestion 提议沉淀。
|
|
140
|
+
|
|
141
|
+
### 5.1 AskUserQuestion 模板(HC-9 锁死)
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
question: "本次结果是否沉淀?"
|
|
145
|
+
options:
|
|
146
|
+
- (a) 沉淀为普通 lesson / convention → peaks memory extract --apply
|
|
147
|
+
- (b) 沉淀为 Loop Engineering 资产 → peaks asset crystallize
|
|
148
|
+
- (c) 改 scope 再沉淀(LLM 继续追问)
|
|
149
|
+
- (d) 不沉淀(单次性,丢弃)
|
|
150
|
+
multiSelect: false
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**默认推荐 = (a)**(普通 lesson / convention),LLM 必须给一段 NL 解释为什么推荐 (a) 或 (b),**不**允许默认推荐 (d)。
|
|
154
|
+
|
|
155
|
+
完整 4-option 模板 + 触发条件矩阵见 `references/sediment-prompt-template.md`。
|
|
156
|
+
|
|
157
|
+
### 5.2 触发条件矩阵
|
|
158
|
+
|
|
159
|
+
| peaks-solo 跑完类型 | 是否问 | trigger |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| 转交 leaf 跑完 | **不问** | (leaf 内部 Step 11 处理) |
|
|
162
|
+
| 自规划兜底跑完 | **默认问** | `success_default_prompt` |
|
|
163
|
+
| 用户主动说"沉淀这个" | **立即问** | `user_explicit` |
|
|
164
|
+
| LLM 识别 ≥ 2 个 reuse signals | **附带 4-section brief 问** | `llm_suggested` |
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 6. Out of scope (HC-11 锁死)
|
|
169
|
+
|
|
170
|
+
peaks-solo 是 dispatcher,**不**是 orchestrator,**不**是 implementer。下面四类操作**禁止**直接在 peaks-solo 内部执行:
|
|
171
|
+
|
|
172
|
+
- **no code** — 不写 / 不改 / 不重构 `src/**` 业务代码,也不调 peaks-rd / peaks-qa / peaks-ui / peaks-sc 子蜂群(那是 peaks-code 的 implementation surface)。遇到 code-domain 需求必须转交 `/peaks-code` 或建议用户自己跑。
|
|
173
|
+
- **no PRD** — 不写产品需求文档(那是 peaks-code → peaks-prd 的范围)。peaks-solo 自己不持有 PRD 的生成 surface。
|
|
174
|
+
- **no vitest** — 不跑测试,也不写 `tests/**` 业务测试。遇到测试需求必须转交 `/peaks-test` 或 `/peaks-code`。
|
|
175
|
+
- **no Loop Engineering Asset mutation** — 不调 `peaks asset crystallize` 改 Loop Engineering 资产,也不写 `.peaks/standards/` / `.peaks/memory/` 之外的 peaks-loop 内部资产(那是用户主权,peak-* 技能层没有改动权)。
|
|
176
|
+
|
|
177
|
+
**唯一例外:** 沉淀提议模板里 **选项 (b)** 提到 `peaks asset crystallize` 是给用户看的,不是 peaks-solo 自动执行;真正执行由用户在 AskUserQuestion 选中后由 LLM 代跑该 CLI。
|
|
178
|
+
|
|
179
|
+
### 6.1 边角 case
|
|
180
|
+
|
|
181
|
+
| case | 处理 |
|
|
182
|
+
|---|---|
|
|
183
|
+
| 用户中途换需求 | 重新跑 §3 triage,不复用旧 candidates |
|
|
184
|
+
| 兜底工具失败(如 WebSearch 不可用) | 转下一种 allowed 工具;3 次失败后 AskUserQuestion 问用户是否要继续 |
|
|
185
|
+
| 沉淀被拒后(用户选 d) | 不重复问,直接关闭本 skill session |
|
|
186
|
+
| 用户在 AskUserQuestion 中输入自由文本(理论上不该发生) | 把它当作 NL 描述,重新跑 §3 triage |
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 相关引用
|
|
191
|
+
|
|
192
|
+
- `references/triage-decision-table.md` — 关键词 → leaf skill 完整映射表(≥ 10 行)
|
|
193
|
+
- `references/fallback-tool-inventory.md` — 自规划允许 + 禁止工具清单
|
|
194
|
+
- `references/sediment-prompt-template.md` — 4-option AskUserQuestion 沉淀模板
|
|
195
|
+
- spec: `docs/superpowers/specs/2026-07-08-peaks-solo-dispatcher-design.md` §3.1, §3.3, §3.4, §3.5
|
|
196
|
+
- plan: `docs/superpowers/plans/2026-07-08-peaks-solo-dispatcher/s1-peaks-solo-skill.md`
|
|
197
|
+
- decision memo: `.peaks/memory/user-decision-2026-07-08-revive-peaks-solo-as-dispatcher.md`
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# peaks-solo — Fallback Tool Inventory (自规划兜底工具清单)
|
|
2
|
+
|
|
3
|
+
> 当 `peaks skill search` 返回 0 候选,或用户在 AskUserQuestion 选"(e) 都不对",peaks-solo 走自规划兜底。本表规定自规划下 LLM **允许**与**禁止**调用的工具。
|
|
4
|
+
> **核心边界(HC-11 锁死):** peaks-solo 不写业务代码、不写 PRD、不跑 vitest、不改 Loop Engineering 资产。下面 blocked 列表是 HC-11 的可执行形式。
|
|
5
|
+
|
|
6
|
+
## Allowed tools (允许)
|
|
7
|
+
|
|
8
|
+
| 工具 | 适用场景 | 限制 |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| `deep-search` skill | 信息查询 / 研究类 / 多跳事实核查 | 若 LLM CLI 平台装了 deep-search 自动出现;peaks-solo **不主动 install**;若未装,降级到 WebSearch |
|
|
11
|
+
| `WebSearch` | 实时性高的查询(超过训练数据)/ trending / top N | 内置工具,无需 install |
|
|
12
|
+
| `WebFetch` | 已知 URL 的内容抓取(单页,无 JS 渲染) | 内置工具;不爬取含登录 / 反爬的页面 |
|
|
13
|
+
| `Bash` | 数值计算 / 系统命令 / git 操作 / `ls` / `cat` / `rg` / `grep` | 仅**只读命令**;不写 src/** 业务代码;不 rm;不 git push --force;不 chmod 777;不 curl \| sh |
|
|
14
|
+
| `Edit` / `Write` | 改 markdown / 改 yaml / 改 `.peaks/memory/<user>/*.md` / 改临时文件 | **只允许**改:`*.md` / `*.yaml` / `*.yml` / `.peaks/memory/`;**不允许**改:`src/**` 业务代码 / `skills/peaks-{code,content,doctor,sop,status,test,resume,ide,final-review,slice-decompose,issue-fix-orchestrator,perf-audit,security-audit,audit}/**` 任何 leaf skill |
|
|
15
|
+
| `peaks memory extract` | 跑完提议沉淀(选项 a) | 走现成 CLI,不重写;不创建新 CLI |
|
|
16
|
+
| `AskUserQuestion` | 多候选分诊 / 沉淀提议 / 兜底方案选择 | HC-9 锁死;**不**引入自由文本输入 |
|
|
17
|
+
|
|
18
|
+
## Blocked tools / operations (禁止)
|
|
19
|
+
|
|
20
|
+
下表是**显式禁止**——LLM 在自规划兜底中**不能**调这些工具或跑这些操作。违反即违反 HC-11。
|
|
21
|
+
|
|
22
|
+
| blocked 操作 | 原因 | 替代方案 |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `rm -rf <path>` | 不可逆破坏性操作 | 改用 `mv <path> <path>.bak` 或只读 `ls` |
|
|
25
|
+
| `git push --force` / `git push -f` | 改写历史,影响其他协作者 | 用 `git push --force-with-lease`(若必须)或拒绝 push 让用户自己来 |
|
|
26
|
+
| `Edit src/**` / `Write src/**` 业务代码 | HC-11: no code;peaks-code 域 | 转交 `/peaks-code` |
|
|
27
|
+
| `Edit skills/peaks-{code,content,doctor,sop,status,test,resume,ide,final-review,slice-decompose,issue-fix-orchestrator,perf-audit,security-audit,audit}/**` 任何文件 | HC-8 + HC-11: leaf skill 文件是用户主权 | 让用户自己改 |
|
|
28
|
+
| `peaks asset crystallize`(直接调用) | Loop Engineering Asset mutation = 用户主权 | 仅在沉淀 AskUserQuestion 中作为**选项 (b)** 提及,不主动执行 |
|
|
29
|
+
| `Edit .peaks/standards/**` / `Write .peaks/standards/**` | standards 是项目层 invariants | 让用户自己改 |
|
|
30
|
+
| `Edit .claude-plugin/marketplace.json` | marketplace 是 install surface | 让用户自己改 / 转交 S2 |
|
|
31
|
+
| `vitest` / `pnpm test` / 任何测试 runner | HC-11: no vitest | 转交 `/peaks-test` 或 `/peaks-code` |
|
|
32
|
+
| `curl <untrusted-url> \| sh` / `wget \| bash` | RCE 风险 | 拒绝执行;若必须,先下载到文件,人工 review 后再执行 |
|
|
33
|
+
| 写入用户 home 目录(`~/.*`) | 用户主权,peaks-solo 不动用户全局配置 | 让用户自己来 |
|
|
34
|
+
|
|
35
|
+
## 边界判断原则
|
|
36
|
+
|
|
37
|
+
LLM 拿不准"该不该做"时,按下面 3 条问自己:
|
|
38
|
+
|
|
39
|
+
1. **这是 peaks-code 域吗?** 是 → 转交 `/peaks-code`。
|
|
40
|
+
2. **这是用户主权领域吗?**(`src/**` 业务代码 / Loop Engineering Asset / 用户 home) → 让用户自己来。
|
|
41
|
+
3. **这是 read-only 还是 mutating?** read-only(`ls` / `cat` / `rg` / WebSearch / WebFetch)→ OK;mutating 且不在 allowed 列表 → 拒绝。
|
|
42
|
+
|
|
43
|
+
## 失败回退
|
|
44
|
+
|
|
45
|
+
- 自规划下某个 allowed 工具失败 3 次(网络 / 权限 / not found)→ 走 AskUserQuestion 问用户:"这个工具一直失败,要不要换另一种?"(HC-9 锁死,不静默放弃)。
|
|
46
|
+
- 自规划全部失败 → 走 AskUserQuestion:"peaks-solo 找不到合适工具,要不要: (a) 换更具体的 NL 重跑 / (b) 转人工 / (c) 结束"。
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# peaks-solo — Sediment Prompt Template (沉淀提议 AskUserQuestion)
|
|
2
|
+
|
|
3
|
+
> 自规划兜底跑完(转交 leaf 跑完则由 leaf 内部 Step 11 处理,**不重复问**)→ peaks-solo 用本模板提议沉淀。
|
|
4
|
+
> **核心约束(HC-9 Human-NL-Choice-Only 锁死):** 必须用 AskUserQuestion 4-option,**不**允许自由文本输入;**默认推荐 = (a)**,**不**是 (d)。
|
|
5
|
+
|
|
6
|
+
## 4-option AskUserQuestion 模板
|
|
7
|
+
|
|
8
|
+
```yaml
|
|
9
|
+
question: "本次结果是否沉淀?"
|
|
10
|
+
options:
|
|
11
|
+
- label: "(a) 沉淀为普通 lesson / convention"
|
|
12
|
+
description: "通过 peaks memory extract --apply 写到 .peaks/memory/<user>/,作为可复用 lessons/conventions。"
|
|
13
|
+
- label: "(b) 沉淀为 Loop Engineering 资产"
|
|
14
|
+
description: "通过 peaks asset crystallize 提升为 Loop Engineering Asset(标准 / skill / SOP / framework)。"
|
|
15
|
+
- label: "(c) 改 scope 再沉淀"
|
|
16
|
+
description: "本次 scope 不对,LLM 继续追问(例如想问清楚是什么 lesson),然后再走 (a) 或 (b)。"
|
|
17
|
+
- label: "(d) 不沉淀"
|
|
18
|
+
description: "单次性结果,丢弃,不写入任何 .peaks/ 路径。"
|
|
19
|
+
multiSelect: false
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 默认推荐
|
|
23
|
+
|
|
24
|
+
**默认推荐 = (a)**(普通 lesson / convention),**不**是 (d)。
|
|
25
|
+
|
|
26
|
+
**为什么推荐 (a) 而不是 (b):** (b) 沉淀为 Loop Engineering Asset 是**重操作**——会进入 `.peaks/standards/` 或 `.peaks/memory/<asset>/`,影响所有 peaks-* skill 的语义。普通 lesson / convention 风险低、可回滚、不会污染项目 invariants,适合作为大多数一次性结果的首选归宿。(b) 仅在 LLM 识别 ≥ 2 个 reuse signals 时推荐。
|
|
27
|
+
|
|
28
|
+
## LLM 必须给一段 NL rationale
|
|
29
|
+
|
|
30
|
+
在调 AskUserQuestion 前,LLM **必须**先(在最终回复里,AskUserQuestion 之前)给一段 1-3 句的 NL 解释,说明为什么推荐 (a) 或 (b)。模板:
|
|
31
|
+
|
|
32
|
+
> "我推荐 (a) 沉淀为普通 lesson / convention,理由:<1 句:本次结果的可复用程度 / 适用范围 / 为什么不是 (b) / 为什么不是 (d)>。如果你认为这是 Loop Engineering 级别的资产,选 (b);如果本次是单次性 / scope 不对,选 (c) 或 (d)。"
|
|
33
|
+
|
|
34
|
+
**禁止:** 直接弹 AskUserQuestion 不解释;**禁止:** 默认推荐 (d)。
|
|
35
|
+
|
|
36
|
+
## 触发条件矩阵
|
|
37
|
+
|
|
38
|
+
| peaks-solo 跑完类型 | 是否问沉淀 | trigger 标记 |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| 转交 leaf 跑完 | **不问**(leaf 内部 Step 11 处理) | `leaf_handled` |
|
|
41
|
+
| 自规划兜底跑完(成功) | **默认问** | `success_default_prompt` |
|
|
42
|
+
| 自规划兜底跑完(部分失败) | **附带"是否继续"二选一 + 沉淀四选一**两个 AskUserQuestion | `partial_failure` |
|
|
43
|
+
| 用户主动说"沉淀这个" / "把这个记下来" | **立即问**(跳过自规划完成检测) | `user_explicit` |
|
|
44
|
+
| LLM 识别 ≥ 2 个 reuse signals(同一结果可在 ≥ 2 个未来场景复用) | **附带 4-section brief 问**(在 question 前先输出 brief:reuse contexts / scope / drift risks / crystallize recommendation) | `llm_suggested` |
|
|
45
|
+
| 沉淀提议被用户选 (d) 后 | **不重复问**,直接关闭 peaks-solo session | `user_declined` |
|
|
46
|
+
| 沉淀提议被用户选 (c) 后(scope 改) | **继续追问 → 再问一次 4-option** | `scope_change` |
|
|
47
|
+
|
|
48
|
+
## 沉淀后的执行
|
|
49
|
+
|
|
50
|
+
| 用户选 | LLM 执行 |
|
|
51
|
+
|---|---|
|
|
52
|
+
| (a) | `peaks memory extract --apply --session-id <sid> --project <repo>` —— 走现成 CLI,不重写 |
|
|
53
|
+
| (b) | `peaks asset crystallize --asset <name> --project <repo>` —— 走现成 CLI;**仅**在用户明确选 (b) 后执行 |
|
|
54
|
+
| (c) | 继续 AskUserQuestion 追问(例如"具体是什么场景的 lesson?" / "适用哪个子项目?") |
|
|
55
|
+
| (d) | 不写任何文件;peaks-solo 直接结束 |
|
|
56
|
+
|
|
57
|
+
## 反 pattern 警告
|
|
58
|
+
|
|
59
|
+
- **不推荐 (b) 默认推荐** —— 大多数一次性结果进不了 Loop Engineering Asset 层;盲目 crystallize 会污染 `.peaks/standards/`。
|
|
60
|
+
- **不静默丢弃** —— 用户没明示"不要沉淀",就必须问;选 (d) 必须是用户**主动选**,不是 LLM 默认。
|
|
61
|
+
- **不重复问** —— 转交 leaf 跑完由 leaf 处理 sediment;peaks-solo 只对**自规划兜底跑完**问。
|
|
62
|
+
- **不让用户自由文本** —— HC-9 锁死;不在 AskUserQuestion 选项之外加 free-form input。
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# peaks-solo — Triage Decision Table
|
|
2
|
+
|
|
3
|
+
> 关键词 → leaf skill 快速映射。peaks-solo 在 §3 triage 决策流里先扫本表做"启发式预判",再用 `peaks skill search --query "<NL>"` 做精确匹配。
|
|
4
|
+
> 匹配规则:case-insensitive substring(命中即行;同 NL 多行命中 → AskUserQuestion 多选)。
|
|
5
|
+
|
|
6
|
+
| keyword | → | leaf skill |
|
|
7
|
+
|---|---|---|
|
|
8
|
+
| code / 代码 / 改 bug / 写功能 / 全流程 / 端到端 / PR / 测试覆盖 / 改 src | → | /peaks-code |
|
|
9
|
+
| 文档 / article / blog / 内容 / 写稿 / 内容工作流 / 公众号 / 长文 | → | /peaks-content |
|
|
10
|
+
| 健康 / health / 体检 / audit / 报告 / doctor report / 项目诊断 | → | /peaks-doctor |
|
|
11
|
+
| issue / 修 issue / sweep / 批量修 / open issue 处理 | → | /peaks-issue-fix-orchestrator |
|
|
12
|
+
| SOP / 流程 / 工作流 / 审批 / 门禁 / 标准操作 | → | /peaks-sop |
|
|
13
|
+
| 性能 / perf / 慢 / 优化 / latency / 响应时间 | → | /peaks-perf-audit (or 自规划) |
|
|
14
|
+
| 安全 / security / 漏洞 / vulnerability / OWASP | → | /peaks-security-audit (or 自规划) |
|
|
15
|
+
| 状态 / status / 进度 / 现在到哪 / 当前在哪一步 | → | /peaks-status |
|
|
16
|
+
| 跑测试 / test / vitest / unit test | → | /peaks-test |
|
|
17
|
+
| 恢复 / resume / 继续 / 接上次 | → | /peaks-resume |
|
|
18
|
+
| 不知道 / 随便 / 帮我 / 你帮我决定 / 我想做点东西 | → | peaks-solo triage (this skill) |
|
|
19
|
+
| 切片 / slice / 拆解任务 / 大任务拆小 | → | /peaks-slice-decompose |
|
|
20
|
+
| 想法 / brain-storm / 创意 / 探索思路 | → | /peaks-ide |
|
|
21
|
+
| 最终审查 / final review / 验收 / 4-dim evidence | → | /peaks-final-review |
|
|
22
|
+
| 研究 / research / 信息查询 / GitHub trending / top 10 / 调查 | → | (no leaf) → 自规划(deep-search / WebSearch) |
|
|
23
|
+
| 一句话需求 / 一次性 / 单次性 | → | peaks-solo 自规划兜底 |
|
|
24
|
+
|
|
25
|
+
## 使用说明
|
|
26
|
+
|
|
27
|
+
1. **先扫表,再跑 search。** 表是 O(N) 的 substring 命中;跑 `peaks skill search` 是兜底。两者结果不一致时,以 search 为准(search 读的是 live frontmatter)。
|
|
28
|
+
2. **多行命中 → AskUserQuestion 多选,**模板见 SKILL.md §3.1。
|
|
29
|
+
3. **(e) "都不对" → 自规划兜底。** 走 SKILL.md §4。
|
|
30
|
+
4. **空表条目:** "研究 / 信息查询 / GitHub trending / top 10" 等一次性信息查询,**当前 skill 池没有专属 leaf**,peaks-solo 直接走自规划(deep-search / WebSearch)。
|
|
31
|
+
5. **本表是 opportunistic 表,**不是 exhaustive 表。S2/S3 后续可在 dogfood 中发现新关键词补行。
|