peaks-loop 4.0.0-beta.3 → 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.
@@ -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,103 @@
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
+
21
+ ## 4.0.0-beta.4 — 2026-07-08
22
+
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.
24
+
25
+ ### Added — Loop Engineering Asset layer (§4.1, AC-1/AC-2/AC-3)
26
+
27
+ - **`loop_release` table** — `src/services/skillhub/migrations/002-loop-release.sql`. Schema version pinned to `peaks.loop/1` via CHECK constraint. Indexes on `lifecycle_status` and `scenario`. Non-breaking: 4.x `bee_release` rows continue to read.
28
+ - **`loop_bee_relation` table** — `src/services/skillhub/migrations/003-loop-bee-relation.sql`. Schema version `peaks.loop-bee-relation/1`. Roles: `main` / `supporting` / `candidate` / `retired`. Partial UNIQUE index `WHERE role='main'` enforces at most one main bee per loop at the storage layer (defense in depth; the service layer also enforces it).
29
+ - **Schema for the four-loop + dual-asset model** — Zod schemas in `src/services/loop/loop-release-types.ts` and `src/services/loop/loop-bee-relation-types.ts` mirror the §4.1 / §4.6 row shapes and refuse to parse rows whose `schema_version` is not the fixed literal.
30
+
31
+ ### Added — Post-run crystallization flow (§5, AC-4 / AC-5 / AC-6 / AC-7)
32
+
33
+ - **`crystallization_event` table** — `src/services/skillhub/migrations/006-crystallization-event.sql`. Schema version `peaks.crystallization/1`. Carries the 4-section evidence brief inline; FKs to optional created/updated loop_release / bee_release.
34
+ - **`CrystallizationService`** — `src/services/crystallization/crystallization-service.ts`. Pre-run gate: `task_status='completed'` AND `gates_passed=true` AND `evidence_collected=true` (Zod literal enforcement + service-layer re-assertion). Atomic single-transaction write of loop_release + bee_release header + bee_manifest + loop_bee_relation(main) + crystallization_event(brief inline).
35
+ - **`peaks asset crystallize`** CLI — `src/cli/commands/asset-commands.ts`. Required options: `--brief-what-happened`, `--brief-why-it-matters`, `--brief-what-learned`, `--brief-what-action`. A partial brief is rejected with `MISSING_BRIEF_SECTION` and exit 1 (RL-7).
36
+ - **`peaks asset dispose` / `peaks asset status`** — cross-asset dispose (trace_only / retain / destroy) and lifecycle status dashboard.
37
+
38
+ ### Added — Darwin-style ratchet (§6, AC-8 / AC-9 / AC-10 / AC-11)
39
+
40
+ - **`evolution_evaluation` table** — `src/services/skillhub/migrations/005-evolution-evaluation.sql`. Schema version `peaks.evolution/1`. Carries `target_kind`, `target_release_id`, `optimization_dimensions_json` (length-1 enforced at service layer), `target_count=1` (CHECK), `author_id`, `evaluator_id`, `skeptic_id` (three independent agents), `verdict` ∈ {`keep`, `revert`, `needs-user-decision`}, `user_confirmation_pointer`, `brief_pointer`, and the four-section brief projection.
41
+ - **`EvolutionService`** — `src/services/evolution/evolution-service.ts`. Hard rules: single object (AC-8) + single optimization dimension (AC-8) + author ≠ evaluator ≠ skeptic (AC-10/AC-12/AC-14) + score_delta >= score_delta_min for `keep` (AC-11, default 1.0) + user_confirmation_pointer required for `keep` (AC-15).
42
+ - **`peaks evolution propose / evaluate / revert / mark-keep / status`** CLI — `src/cli/commands/evolution-commands.ts`. Each error path emits a stable wire-format code (`EVOLUTION_MULTI_OBJECT` / `EVOLUTION_MULTI_DIMENSION` / `EVOLUTION_SELF_SCORE` / `EVOLUTION_DELTA_BELOW_THRESHOLD` / `EVOLUTION_MISSING_USER_CONFIRMATION`).
43
+ - **Independent-evaluator runner** — `src/services/evolution/independent-evaluator-runner.ts`. Frozen `EvaluationPackage` containing only `target_kind / target_release_id / optimization_dimension / before_snapshot / after_snapshot / diff / rubric / red_lines / source_traces` — no author session, no author reasoning, no recommendation framing (AC-12 / AC-13).
44
+ - **Regression-skeptic runner** — `src/services/evolution/regression-skeptic-runner.ts`. A SEPARATE agent that emits `driftRisks / overfitRisks / safetyRegressionRisks` plus an optional `blocker` (which forces `verdict='revert'`).
45
+
46
+ ### Added — Karpathy-engineered red-line set (AC-21 / AC-22 / AC-23)
47
+
48
+ - **`.peaks/standards/loop-engineering-guidelines.md`** — the single source of truth for the 10 red lines RL-0..RL-9, each in the four-section karpathy form (Failure modes / Rewrite / Self-check / Out-of-scope). Co-equal karpathy × darwin layers (RL-0).
49
+ - **`peaks standards lint --category loop-engineering`** — `src/services/standards/loop-engineering-lint.ts`. Parses the guideline file and asserts every red line has all four sections, fails closed otherwise.
50
+ - **`peaks skill lint --category loop-engineering-readiness --path <skill-dir>`** — `src/services/standards/loop-engineering-readiness-lint.ts` + `src/cli/commands/skill-loop-engineering-readiness-commands.ts`. Asserts a peaks-* SKILL.md (a) references `.peaks/standards/loop-engineering-guidelines.md`, (b) does not introduce a CLI verb the user is meant to type, (c) does not introduce a JSON / manifest hand-authoring surface. Alias verb: `peaks skill ready --category loop-engineering-readiness --path <skill-dir>`.
51
+ - **Unit guard** — `tests/unit/standards/loop-engineering-guidelines.test.ts` enforces the four-section shape for every red line in the file.
52
+ - **peaks-code domain boundary (RL-8)** — `skills/peaks-code/SKILL.md` self-declares as the code-domain long-task loop engineering orchestrator and is NOT a general-purpose orchestrator. Cross-domain peaks-* skills (peaks-content, peaks-issue-fix-orchestrator) import the shared guideline file and pass the readiness lint.
53
+
54
+ ### Added — Bundle share + desktop extension surface (§7A, AC-24 / AC-25 / AC-26)
55
+
56
+ - **Share / desktop fields** — `src/services/skillhub/migrations/004-loop-bee-extension.sql`. Adds `shareable / share_excluded_paths / desktop_visible / export_bundle_format` to `loop_release`, and `shareable / desktop_visible` to `bee_release`. `export_bundle_format` is pinned to `peaks.bundle/1` via CHECK constraint. Non-breaking: every new column has a DEFAULT.
57
+ - **Bundle writer** — `src/services/share/bundle-writer.ts`. Writes a `peaks.bundle/1` tar.gz with `manifest.json`, `relations.json`, `evidence_briefs/*.json`, content-addressed `blobs/<sha256>`, and `EVALUATION_REQUIRED.md`. Hard block: `shareable=false` throws `BundleNotShareableError(SHARE_BUNDLE_ERROR_CODES.NOT_SHAREABLE)` BEFORE any tarball work, for both loop and bee kinds.
58
+ - **Bundle reader** — `src/services/share/bundle-reader.ts`. Hard blocks: `format_version_major != 1` is rejected (AC-25); `schema_versions` mismatch is rejected; the imported release ALWAYS lands as `candidate` (AC-25 / §10 RL-9). The bundle carries an `EVALUATION_REQUIRED.md` marker telling the receiver to run an independent evaluation before any durable change.
59
+ - **`peaks loop export` / `peaks loop import`** — `src/cli/commands/loop-commands.ts`. Existing 14.x `peaks loop *` subcommands are preserved; M7 only ADDS export / import.
60
+ - **`peaks bee export` / `peaks bee import`** — `src/cli/commands/share-commands.ts`. Same hard-block semantics.
61
+ - **Receiver-side integration test** — `tests/integration/share-bundle-roundtrip.test.ts` asserts the round-trip lands as `candidate` (AC-25), that `shareable=false` blocks the export at the CLI layer, and that without an `evolution_evaluation` row the receiver has no path to `stable` (AC-26).
62
+
63
+ ### Added — SkillHub expansion (5 new tables / non-breaking)
64
+
65
+ The 6+ relation table layout is preserved. 5 new tables added under `.peaks/_runtime/<sessionId>/`:
66
+ - `loop_release` (002)
67
+ - `loop_bee_relation` (003)
68
+ - loop↔bee share / desktop extension columns (004)
69
+ - `evolution_evaluation` (005)
70
+ - `crystallization_event` (006)
71
+
72
+ Migration is non-breaking: every new column has a DEFAULT and the new tables sit alongside the existing 4.x schema.
73
+
74
+ ### Added — New CLI verbs (spec §7.4)
75
+
76
+ - `peaks loop init / list / show / search / recent / export / import`
77
+ - `peaks asset crystallize / dispose / status`
78
+ - `peaks evolution propose / evaluate / revert / mark-keep / status`
79
+ - `peaks skill lint --category loop-engineering-readiness --path <skill-dir>`
80
+ - `peaks skill ready --category loop-engineering-readiness --path <skill-dir>` (alias)
81
+
82
+ ### Changed
83
+
84
+ - **`peaks workflow *`** is reframed as the execution trace surface (per spec §7.6 demotion); it remains functional. The user-facing verb is "replay this run", not "create a new asset".
85
+ - **peaks-maker skill** is narratively repositioned to "Loop crystallizer + Bee creator + Evolution gatekeeper" (per spec §7.5); only the SKILL.md and memory references are re-narrated, the `id` is preserved.
86
+
87
+ ### Verification (4.0.0-beta.4)
88
+
89
+ - `tsc --noEmit` zero errors
90
+ - `peaks standards lint --category loop-engineering` parses all 10 red lines + 4 sections each (lint passes)
91
+ - `pnpm vitest run tests/unit/{standards,crystallization,evolution,share}/` — 13 files / 154 unit tests pass
92
+ - `pnpm vitest run tests/integration/{share-bundle-roundtrip,dogfood-loop-engineering-crystallization,asset-crystallize-cli,evolution-cli,skill-loop-engineering-readiness-cli}.test.ts` — 5 files / 26 integration tests pass
93
+ - M8 dogfood (`tests/integration/dogfood-loop-engineering-crystallization.test.ts`) crystallizes the M0..M7 work into a real loop + bee + relation + event in 644ms (defense in depth: the release crystallizes itself)
94
+
95
+ ### Migration
96
+
97
+ - 4.x `bee_release` rows continue to read. No data migration required.
98
+ - The 5 new tables are added via non-destructive `CREATE TABLE IF NOT EXISTS` migrations.
99
+ - The `bundle-writer / bundle-reader` pair is additive; existing `peaks skill sediment export / import` remains as an alias for one release cycle, then deprecates.
100
+ - The peaks-code SKILL.md is unchanged from 4.0.0-beta.3 — no `peaks-solo` migration needed for users already on beta.2 or later.
101
+
5
102
  ## 4.0.0-beta.2 — 2026-07-07
6
103
 
7
104
  ### Renamed
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.3";
1
+ export declare const CLI_VERSION = "4.0.0-beta.5";
@@ -1 +1 @@
1
- export const CLI_VERSION = "4.0.0-beta.3";
1
+ export const CLI_VERSION = "4.0.0-beta.5";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-loop",
3
- "version": "4.0.0-beta.3",
3
+ "version": "4.0.0-beta.5",
4
4
  "description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
5
5
  "author": "SquabbyZ",
6
6
  "license": "MIT",
@@ -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 中发现新关键词补行。