peaks-cli 1.4.1 → 1.4.2

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.
Files changed (57) hide show
  1. package/README.md +0 -53
  2. package/dist/src/cli/commands/core-artifact-commands.js +21 -0
  3. package/dist/src/cli/commands/memory-commands.d.ts +13 -0
  4. package/dist/src/cli/commands/memory-commands.js +60 -0
  5. package/dist/src/cli/commands/retrospective-commands.d.ts +9 -0
  6. package/dist/src/cli/commands/retrospective-commands.js +58 -0
  7. package/dist/src/cli/program.js +16 -22
  8. package/dist/src/services/fuzzy-matching/fuzzy-match-service.d.ts +15 -0
  9. package/dist/src/services/fuzzy-matching/fuzzy-match-service.js +56 -0
  10. package/dist/src/services/fuzzy-matching/types.d.ts +20 -0
  11. package/dist/src/services/fuzzy-matching/types.js +1 -0
  12. package/dist/src/services/memory/memory-search-service.d.ts +61 -0
  13. package/dist/src/services/memory/memory-search-service.js +80 -0
  14. package/dist/src/services/recommendations/capability-seed-items.js +0 -1
  15. package/dist/src/services/recommendations/capability-seed-mappings.js +0 -1
  16. package/dist/src/services/recommendations/capability-seed-sources.js +0 -1
  17. package/dist/src/services/retrospective/retrospective-search-service.d.ts +37 -0
  18. package/dist/src/services/retrospective/retrospective-search-service.js +75 -0
  19. package/dist/src/services/standards/project-context.d.ts +1 -1
  20. package/dist/src/services/standards/project-context.js +0 -4
  21. package/dist/src/services/standards/project-standards-service.js +1 -3
  22. package/dist/src/services/workspace/migrate-1-4-1-service.js +1 -1
  23. package/dist/src/shared/version.d.ts +1 -1
  24. package/dist/src/shared/version.js +1 -1
  25. package/package.json +3 -7
  26. package/skills/peaks-solo/SKILL.md +1 -1
  27. package/skills/peaks-solo/references/completion-handoff.md +3 -1
  28. package/dist/src/cli/commands/shadcn-commands.d.ts +0 -3
  29. package/dist/src/cli/commands/shadcn-commands.js +0 -35
  30. package/dist/src/cli/commands/skill-context-stats-command.d.ts +0 -40
  31. package/dist/src/cli/commands/skill-context-stats-command.js +0 -96
  32. package/dist/src/cli/commands/skill-scope-commands.d.ts +0 -51
  33. package/dist/src/cli/commands/skill-scope-commands.js +0 -310
  34. package/dist/src/services/shadcn/shadcn-service.d.ts +0 -27
  35. package/dist/src/services/shadcn/shadcn-service.js +0 -128
  36. package/dist/src/services/skill-scope/adapters/_stub-helper.d.ts +0 -39
  37. package/dist/src/services/skill-scope/adapters/_stub-helper.js +0 -98
  38. package/dist/src/services/skill-scope/adapters/claude-code.d.ts +0 -59
  39. package/dist/src/services/skill-scope/adapters/claude-code.js +0 -304
  40. package/dist/src/services/skill-scope/adapters/codex.d.ts +0 -2
  41. package/dist/src/services/skill-scope/adapters/codex.js +0 -12
  42. package/dist/src/services/skill-scope/adapters/cursor.d.ts +0 -2
  43. package/dist/src/services/skill-scope/adapters/cursor.js +0 -13
  44. package/dist/src/services/skill-scope/adapters/qoder.d.ts +0 -2
  45. package/dist/src/services/skill-scope/adapters/qoder.js +0 -13
  46. package/dist/src/services/skill-scope/adapters/tongyi.d.ts +0 -2
  47. package/dist/src/services/skill-scope/adapters/tongyi.js +0 -13
  48. package/dist/src/services/skill-scope/adapters/trae.d.ts +0 -2
  49. package/dist/src/services/skill-scope/adapters/trae.js +0 -12
  50. package/dist/src/services/skill-scope/detect.d.ts +0 -81
  51. package/dist/src/services/skill-scope/detect.js +0 -513
  52. package/dist/src/services/skill-scope/registry.d.ts +0 -41
  53. package/dist/src/services/skill-scope/registry.js +0 -83
  54. package/dist/src/services/skill-scope/source-of-truth.d.ts +0 -44
  55. package/dist/src/services/skill-scope/source-of-truth.js +0 -118
  56. package/dist/src/services/skill-scope/types.d.ts +0 -195
  57. package/dist/src/services/skill-scope/types.js +0 -97
package/README.md CHANGED
@@ -148,59 +148,6 @@ peaks project memories --project <repo> --json # 读取 .peaks/memory/ 里
148
148
 
149
149
  完整命令列表跑 `peaks --help` 即可。
150
150
 
151
- ## 技能白名单:`peaks skill scope`
152
-
153
- `peaks skill scope` 把"哪些 skill 暴露给当前项目的 LLM"这件事从"全部加载"变成"按项目相关度过滤"。1.4.0 起默认带 shadow-stub 机制,**denied skill 的 LLM 上下文减量约 96.4%**(每个 stub ≈285 字节 vs 原 SKILL.md ≈7-8 KB)。
154
-
155
- ### 三步走
156
-
157
- ```bash
158
- peaks skill scope --detect --project <repo> --json # 看哪些 skill 被标 relevant / borderline / irrelevant
159
- peaks skill scope --apply --project <repo> # 写 source-of-truth + project-local mirror(默认 shadow-stub)
160
- peaks skill scope --show --project <repo> # 查当前应用了哪个 allowlist
161
- peaks skill scope --reset --project <repo> # 全部撤回
162
- ```
163
-
164
- `--apply` 写两份东西:
165
- - `.peaks/scope/skills.json` — 唯一真源(allowlist + meta)
166
- - `.claude/skills/<skill>/SKILL.md` — project-local mirror;denied skill 被替换为 ~285 字节的 shadow stub,前 5 行包含 `description: _peaks_scope_disabled` 标记,Claude Code 看到这个标记就跳过它的 full body
167
-
168
- ### 怎么 `--apply` 真的把上下文砍下来
169
-
170
- 光把 skill 从 LLM 的可调用列表里 deny 不够——Claude Code 仍然会从项目本地 mirror 读完整的 SKILL.md。**shadow-stub** 把 mirror 也换成 5 行 stub:
171
-
172
- ```yaml
173
- ---
174
- name: agent-sort
175
- description: _peaks_scope_disabled
176
- peaks_scope_disabled: true
177
- ---
178
- # Disabled by `peaks skill scope --apply`
179
- ```
180
-
181
- 实测在 ice-cola 项目(pnpm monorepo,4 包,TS + Python):215 个 skill × 1.92 MB → 应用白名单后 44 个 allowed × 364 KB + 123 个 denied × 35 KB stubs = **399 KB 整体 LLM 可见 skill 上下文**。**减量 924.7 KB / 69.8%**,其中 denied 侧减量 96.4%。
182
-
183
- shadow-stub 是 1.4.0 的默认行为(`--shadow-fallback`)。想 copy 原 SKILL.md 到 mirror(IDE 端 inspection 用),传 `--no-shadow-fallback` 显式 opt-out。
184
-
185
- ### 运行时观测:`peaks skill context-stats`
186
-
187
- 想知道当前项目实际加载了多少 skill 字节 + 估计 token 数:
188
-
189
- ```bash
190
- peaks skill context-stats --project <repo>
191
- # Allowed: 44 skills, 364.4 KB / 91.1K tokens.
192
- # Denied: 123 skills, 35.0 KB / 8.8K tokens (96.4% shadow-stub reduction).
193
- # Total: 399.5 KB / 99.9K tokens.
194
- ```
195
-
196
- 加 `--json` 拿结构化 envelope。还没 apply scope 的项目会拿到 `NO_SCOPE` code + 推荐命令。
197
-
198
- ### 限制
199
-
200
- - detect 的 `relevant` 标记是按文件后缀占比 ≥ 5% 才算的(1.4.1 起的 shareByExtension 阈值)。比如冰-cola 这种 TS + Python 单仓,cpp/golang/java/kotlin/rust/swift 这些 language-specific skill 不会被标 relevant;以前 1 个 stray `.cpp` 文件就会把 `cpp-coding-standards` 误判为 relevant。
201
- - 阈值可通过 `PEAKS_SCOPE_THRESHOLD=0.05` 环境变量或 `--threshold 0.05` 调整。
202
- - `.peaks/scope/skills.json` 是 hand-editable 的;手改后下次 `--apply` 会按 detect 结果覆盖(除非带 `--strict` / `--loose` 改 detect 模式,不带 allowOverride)。
203
-
204
151
  ## 自定义 SOP(把你的流程变成带门禁的工作流)
205
152
 
206
153
  > **技能入口**:`peaks-sop` 技能
@@ -519,6 +519,27 @@ export function registerCoreAndArtifactCommands(program, io) {
519
519
  process.exitCode = 1;
520
520
  }
521
521
  });
522
+ addJsonOption(memory
523
+ .command('search <query>')
524
+ .description('Fuzzy-search the memory index (deterministic, local, zero-token). Default --limit 6.')
525
+ .option('--kind <kind>', 'filter by memory kind (one of: project, rule, decision, reference, feedback, convention, module, lesson)')
526
+ .option('--limit <n>', 'maximum number of matches to return', (value) => Number(value))
527
+ .option('--project <path>', 'target project root (defaults to git root or cwd)')).action((query, options) => {
528
+ // Lazy import avoids a top-of-file import cycle (memory-commands.ts
529
+ // imports services that the rest of this file may also touch).
530
+ void import('./memory-commands.js').then(({ runMemorySearch }) => {
531
+ runMemorySearch(io, {
532
+ query,
533
+ ...(options.kind !== undefined ? { kind: options.kind } : {}),
534
+ ...(options.limit !== undefined ? { limit: options.limit } : {}),
535
+ ...(options.project !== undefined ? { project: options.project } : {}),
536
+ ...(options.json !== undefined ? { json: options.json } : {}),
537
+ });
538
+ }).catch((error) => {
539
+ printResult(io, fail('memory.search', 'MEMORY_SEARCH_BOOTSTRAP_FAILED', getErrorMessage(error), {}, []), options.json);
540
+ process.exitCode = 1;
541
+ });
542
+ });
522
543
  const proxy = program.command('proxy').description('Manage proxy settings');
523
544
  addJsonOption(proxy
524
545
  .command('test')
@@ -0,0 +1,13 @@
1
+ import { type ProgramIO } from '../cli-helpers.js';
2
+ export interface MemorySearchCommandOptions {
3
+ query: string;
4
+ kind?: string;
5
+ limit?: number;
6
+ project?: string;
7
+ json?: boolean;
8
+ }
9
+ /**
10
+ * Run the memory search subcommand. Extracted so unit tests can
11
+ * exercise the full envelope without spawning a subprocess.
12
+ */
13
+ export declare function runMemorySearch(io: ProgramIO, options: MemorySearchCommandOptions): void;
@@ -0,0 +1,60 @@
1
+ import { findProjectRoot } from '../../services/config/config-safety.js';
2
+ import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
3
+ import { searchMemory } from '../../services/memory/memory-search-service.js';
4
+ import { fail, ok } from '../../shared/result.js';
5
+ import { getErrorMessage, printResult } from '../cli-helpers.js';
6
+ const VALID_KINDS = [
7
+ 'project',
8
+ 'rule',
9
+ 'decision',
10
+ 'reference',
11
+ 'feedback',
12
+ 'convention',
13
+ 'module',
14
+ 'lesson',
15
+ ];
16
+ /**
17
+ * Run the memory search subcommand. Extracted so unit tests can
18
+ * exercise the full envelope without spawning a subprocess.
19
+ */
20
+ export function runMemorySearch(io, options) {
21
+ const projectRoot = options.project !== undefined
22
+ ? resolveCanonicalProjectRoot(options.project)
23
+ : (findProjectRoot(process.cwd()) ?? process.cwd());
24
+ const kindFilter = options.kind !== undefined && VALID_KINDS.includes(options.kind)
25
+ ? options.kind
26
+ : undefined;
27
+ // When the user passes --kind but the value isn't in the valid set,
28
+ // we silently pass `undefined` so the search returns the full set;
29
+ // that's friendlier than a hard error and matches the spec's
30
+ // "invalid kind -> empty matches" semantic for the filter path.
31
+ // (For the loader unit test we exercise the explicit-invalid path
32
+ // directly; here the CLI side is forgiving.)
33
+ try {
34
+ const matches = searchMemory({
35
+ query: options.query,
36
+ projectRoot,
37
+ ...(options.limit !== undefined ? { limit: options.limit } : {}),
38
+ ...(kindFilter !== undefined ? { kind: kindFilter } : {}),
39
+ });
40
+ printResult(io, ok('memory.search', {
41
+ query: options.query,
42
+ total: matches.length,
43
+ matches,
44
+ warnings: [],
45
+ }, []), options.json);
46
+ }
47
+ catch (error) {
48
+ const message = getErrorMessage(error);
49
+ const code = error.code ?? 'MEMORY_SEARCH_FAILED';
50
+ const suggestions = [];
51
+ if (code === 'INDEX_MISSING') {
52
+ suggestions.push('Run `peaks memory extract --apply` to build the index from memory/*.md files');
53
+ }
54
+ if (code === 'EMPTY_QUERY') {
55
+ suggestions.push('Use `peaks memory index` to list all entries');
56
+ }
57
+ printResult(io, fail('memory.search', code, message, { projectRoot, query: options.query }, suggestions), options.json);
58
+ process.exitCode = 1;
59
+ }
60
+ }
@@ -1,3 +1,12 @@
1
1
  import { Command } from 'commander';
2
2
  import { type ProgramIO } from '../cli-helpers.js';
3
+ export interface RetrospectiveSearchCommandOptions {
4
+ query: string;
5
+ type?: string;
6
+ outcome?: string;
7
+ limit?: number;
8
+ project?: string;
9
+ json?: boolean;
10
+ }
11
+ export declare function runRetrospectiveSearch(io: ProgramIO, options: RetrospectiveSearchCommandOptions): void;
3
12
  export declare function registerRetrospectiveCommands(program: Command, io: ProgramIO): void;
@@ -3,8 +3,50 @@ import { resolveCanonicalProjectRoot } from '../../services/config/config-servic
3
3
  import { loadRetrospectiveIndex } from '../../services/retrospective/retrospective-index.js';
4
4
  import { showRetrospective } from '../../services/retrospective/retrospective-show.js';
5
5
  import { migrateRetrospectiveFromMd } from '../../services/retrospective/migrate-from-md.js';
6
+ import { searchRetrospective } from '../../services/retrospective/retrospective-search-service.js';
6
7
  import { fail, ok } from '../../shared/result.js';
7
8
  import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
9
+ const VALID_RETRO_TYPES = ['refactor', 'feature', 'bugfix', 'config', 'docs', 'chore'];
10
+ const VALID_RETRO_OUTCOMES = ['shipped', 'blocked', 'in-flight', 'cancelled'];
11
+ export function runRetrospectiveSearch(io, options) {
12
+ const projectRoot = options.project !== undefined
13
+ ? resolveCanonicalProjectRoot(options.project)
14
+ : (findProjectRoot(process.cwd()) ?? process.cwd());
15
+ const typeFilter = options.type !== undefined && VALID_RETRO_TYPES.includes(options.type)
16
+ ? options.type
17
+ : undefined;
18
+ const outcomeFilter = options.outcome !== undefined && VALID_RETRO_OUTCOMES.includes(options.outcome)
19
+ ? options.outcome
20
+ : undefined;
21
+ try {
22
+ const matches = searchRetrospective({
23
+ query: options.query,
24
+ projectRoot,
25
+ ...(typeFilter !== undefined ? { type: typeFilter } : {}),
26
+ ...(outcomeFilter !== undefined ? { outcome: outcomeFilter } : {}),
27
+ ...(options.limit !== undefined ? { limit: options.limit } : {}),
28
+ });
29
+ printResult(io, ok('retrospective.search', {
30
+ query: options.query,
31
+ total: matches.length,
32
+ matches,
33
+ warnings: [],
34
+ }, []), options.json);
35
+ }
36
+ catch (error) {
37
+ const message = getErrorMessage(error);
38
+ const code = error.code ?? 'RETROSPECTIVE_SEARCH_FAILED';
39
+ const suggestions = [];
40
+ if (code === 'INDEX_MISSING') {
41
+ suggestions.push('Run `peaks retrospective migrate --apply` to build the index from legacy MDs');
42
+ }
43
+ if (code === 'EMPTY_QUERY') {
44
+ suggestions.push('Use `peaks retrospective index` to list all entries');
45
+ }
46
+ printResult(io, fail('retrospective.search', code, message, { projectRoot, query: options.query }, suggestions), options.json);
47
+ process.exitCode = 1;
48
+ }
49
+ }
8
50
  export function registerRetrospectiveCommands(program, io) {
9
51
  const retrospective = program.command('retrospective').description('Read the peaks retrospective index (R3: index.json, not the legacy <id>/ MD tree)');
10
52
  addJsonOption(retrospective
@@ -110,4 +152,20 @@ export function registerRetrospectiveCommands(program, io) {
110
152
  process.exitCode = 1;
111
153
  }
112
154
  });
155
+ addJsonOption(retrospective
156
+ .command('search <query>')
157
+ .description('Fuzzy-search the retrospective index (deterministic, local, zero-token). Default --limit 6.')
158
+ .option('--type <type>', `filter by retrospective type (one of: ${VALID_RETRO_TYPES.join(', ')})`)
159
+ .option('--outcome <outcome>', `filter by retrospective outcome (one of: ${VALID_RETRO_OUTCOMES.join(', ')})`)
160
+ .option('--limit <n>', 'maximum number of matches to return', (value) => Number(value))
161
+ .option('--project <path>', 'target project root (defaults to git root or cwd)')).action((query, options) => {
162
+ runRetrospectiveSearch(io, {
163
+ query,
164
+ ...(options.type !== undefined ? { type: options.type } : {}),
165
+ ...(options.outcome !== undefined ? { outcome: options.outcome } : {}),
166
+ ...(options.limit !== undefined ? { limit: options.limit } : {}),
167
+ ...(options.project !== undefined ? { project: options.project } : {}),
168
+ ...(options.json !== undefined ? { json: options.json } : {}),
169
+ });
170
+ });
113
171
  }
@@ -16,7 +16,6 @@ import { registerProjectCommands } from './commands/project-commands.js';
16
16
  import { registerRequestCommands } from './commands/request-commands.js';
17
17
  import { registerRetrospectiveCommands } from './commands/retrospective-commands.js';
18
18
  import { registerScanCommands } from './commands/scan-commands.js';
19
- import { registerShadcnCommands } from './commands/shadcn-commands.js';
20
19
  import { registerSliceCommands } from './commands/slice-commands.js';
21
20
  import { registerSopCommands } from './commands/sop-commands.js';
22
21
  import { registerSubAgentCommands } from './commands/sub-agent-commands.js';
@@ -27,7 +26,6 @@ import { registerHooksCommands } from './commands/hooks-commands.js';
27
26
  import { registerStatusLineCommands } from './commands/statusline-commands.js';
28
27
  import { registerUnderstandCommands } from './commands/understand-commands.js';
29
28
  import { registerWorkspaceCommands } from './commands/workspace-commands.js';
30
- import { registerSkillScopeCommands } from './commands/skill-scope-commands.js';
31
29
  import { registerWorkflowPlanCommands } from './commands/workflow-plan-commands.js';
32
30
  export { printResult } from './cli-helpers.js';
33
31
  export function createProgram(io = { stdout: (text) => console.log(text), stderr: (text) => console.error(text) }) {
@@ -37,13 +35,13 @@ export function createProgram(io = { stdout: (text) => console.log(text), stderr
37
35
  .description(`Peaks CLI ${CLI_VERSION} — workflow-gating CLI + skill family for Claude Code
38
36
 
39
37
  Run peaks (no arguments) for a quickstart. You likely want one of:
40
- peaks doctor check your environment
41
- peaks skill list or manage skills
42
- peaks slice boundary check (tsc + vitest + 3-way + verify-pipeline)
43
- peaks workflow plan workflow routing dry-run graphs
44
- peaks sop author your own workflow gates
45
- peaks hooks install the un-bypassable gate-enforcement hook
46
- peaks gate enforce/bypass SOP gates on Bash commands`)
38
+ peaks doctor check your environment
39
+ peaks skill list or manage skills
40
+ peaks slice boundary check (tsc + vitest +3-way + verify-pipeline)
41
+ peaks workflow plan workflow routing dry-run graphs
42
+ peaks sop author your own workflow gates
43
+ peaks hooks install the un-bypassable gate-enforcement hook
44
+ peaks gate enforce/bypass SOP gates on Bash commands`)
47
45
  .configureOutput({
48
46
  writeOut: (text) => io.stdout(text.trimEnd()),
49
47
  writeErr: (text) => io.stderr(text.trimEnd())
@@ -69,19 +67,19 @@ Run peaks (no arguments) for a quickstart. You likely want one of:
69
67
  }
70
68
  }
71
69
  catch { /* disk read is best-effort; zero skills is still truthful */ }
72
- io.stdout(`Peaks CLI ${CLI_VERSION} · ${skillCount} skills ready
70
+ io.stdout(`Peaks CLI ${CLI_VERSION} · ${skillCount} skills ready
73
71
 
74
- Peaks is a workflow-gating CLI + skill family for Claude Code.
75
- It turns "don't skip steps" into hard enforcement — gates that block
76
- advancement in-conversation, un-bypassably.
72
+ Peaks is a workflow-gating CLI + skill family for Claude Code.
73
+ It turns "don't skip steps" into hard enforcement — gates that block
74
+ advancement in-conversation, un-bypassably.
77
75
 
78
- Before diving into a project, two things worth doing now:
76
+ Before diving into a project, two things worth doing now:
79
77
 
80
- peaks doctor check your environment in one glance
81
- peaks-sop <<< ask this skill to author your first SOP
78
+ peaks doctor check your environment in one glance
79
+ peaks-sop <<< ask this skill to author your first SOP
82
80
 
83
- Or jump straight in:
84
- peaks sop init --id my-flow --apply && peaks hooks install
81
+ Or jump straight in:
82
+ peaks sop init --id my-flow --apply && peaks hooks install
85
83
  `);
86
84
  })
87
85
  .exitOverride();
@@ -95,7 +93,6 @@ Run peaks (no arguments) for a quickstart. You likely want one of:
95
93
  registerRequestCommands(program, io);
96
94
  registerRetrospectiveCommands(program, io);
97
95
  registerScanCommands(program, io);
98
- registerShadcnCommands(program, io);
99
96
  registerSliceCommands(program, io);
100
97
  registerSopCommands(program, io);
101
98
  registerSubAgentCommands(program, io);
@@ -109,9 +106,6 @@ Run peaks (no arguments) for a quickstart. You likely want one of:
109
106
  registerStatusLineCommands(program, io);
110
107
  registerUnderstandCommands(program, io);
111
108
  registerWorkspaceCommands(program, io);
112
- // Slice 025: peaks skill scope — per-project multi-IDE skill scoping.
113
- registerSkillScopeCommands(program, io);
114
- // Slice 025: peaks workflow plan — security/perf plan/result split CLI.
115
109
  registerWorkflowPlanCommands(program, io);
116
110
  return program;
117
111
  }
@@ -0,0 +1,15 @@
1
+ import type { FuzzyMatchOptions, FuzzyMatchResult } from './types.js';
2
+ /**
3
+ * String-overload: when `items` is an array of strings, the searchable text
4
+ * is the string itself. No keyFn is required.
5
+ */
6
+ export declare function fuzzyMatch<T extends string>(query: string, items: T[], options?: FuzzyMatchOptions): FuzzyMatchResult<T>[];
7
+ /**
8
+ * Object-overload: caller provides a `keyFn` that extracts the searchable
9
+ * text from each item. The keyFn is invoked once per item per call; the
10
+ * caller is responsible for ensuring the result is stable (e.g., don't
11
+ * concatenate mutable fields).
12
+ */
13
+ export declare function fuzzyMatchWithKey<T>(query: string, items: T[], options: FuzzyMatchOptions & {
14
+ keyFn: (item: T) => string;
15
+ }): FuzzyMatchResult<T>[];
@@ -0,0 +1,56 @@
1
+ import { Fzf } from 'fzf';
2
+ /**
3
+ * Default limit for fuzzy-match. Aligned with the spec's "--limit default 6".
4
+ */
5
+ const DEFAULT_LIMIT = 6;
6
+ /**
7
+ * String-overload: when `items` is an array of strings, the searchable text
8
+ * is the string itself. No keyFn is required.
9
+ */
10
+ export function fuzzyMatch(query, items, options = {}) {
11
+ return fuzzyMatchWithKey(query, items, { ...options, keyFn: (item) => item });
12
+ }
13
+ /**
14
+ * Object-overload: caller provides a `keyFn` that extracts the searchable
15
+ * text from each item. The keyFn is invoked once per item per call; the
16
+ * caller is responsible for ensuring the result is stable (e.g., don't
17
+ * concatenate mutable fields).
18
+ */
19
+ export function fuzzyMatchWithKey(query, items, options) {
20
+ const { keyFn } = options;
21
+ const limit = options.limit ?? DEFAULT_LIMIT;
22
+ if (items.length === 0)
23
+ return [];
24
+ // Empty query: surface all items (capped at limit) with neutral score and
25
+ // empty positions. Useful for "list" or "preview" use cases where the
26
+ // caller wants a deterministic top-N without a query.
27
+ if (query === '') {
28
+ return items.slice(0, limit).map((item) => ({ item, score: 0, positions: [] }));
29
+ }
30
+ const fzf = new Fzf(items, {
31
+ selector: keyFn,
32
+ limit,
33
+ // Per spec: default is case-insensitive (NOT fzf's smart-case).
34
+ // The user explicitly opts into case-sensitive via caseSensitive:true.
35
+ casing: options.caseSensitive === true ? 'case-sensitive' : 'case-insensitive',
36
+ // normalize:true (default) strips diacritics; fzf returns more matches
37
+ // for non-ASCII text this way, which is what we want for
38
+ // bilingual (zh-CN + en) memory entries.
39
+ });
40
+ const raw = fzf.find(query);
41
+ if (raw.length === 0)
42
+ return [];
43
+ // fzf-for-js score is "higher = better". Normalize so the top of the
44
+ // current batch is exactly 1.0 and others are in [0, 1].
45
+ // When the top score is 0 (degenerate — exact-character-only query that
46
+ // still matched somehow), fall back to 1.0 to avoid divide-by-zero.
47
+ const topScore = raw[0]?.score ?? 1;
48
+ const denom = topScore > 0 ? topScore : 1;
49
+ return raw.slice(0, limit).map((entry) => {
50
+ const score = topScore > 0 ? Number((entry.score / denom).toFixed(4)) : 1;
51
+ // positions is a Set<number> in fzf-for-js; convert to a sorted array
52
+ // so the JSON envelope is stable and human-readable.
53
+ const positions = [...entry.positions].sort((a, b) => a - b);
54
+ return { item: entry.item, score, positions };
55
+ });
56
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Options for the generic fuzzy-match kernel.
3
+ */
4
+ export interface FuzzyMatchOptions {
5
+ /** Maximum number of matches to return. Default 6. */
6
+ limit?: number;
7
+ /** When true, matching is case-sensitive. Default false (smart-case). */
8
+ caseSensitive?: boolean;
9
+ }
10
+ /**
11
+ * A single fuzzy-match hit. `item` is the original entry; `score` is
12
+ * normalized to [0, 1] with the top of the current batch at 1.0;
13
+ * `positions` is the set of char indices in the searchable text that
14
+ * contributed to the match.
15
+ */
16
+ export interface FuzzyMatchResult<T> {
17
+ item: T;
18
+ score: number;
19
+ positions: number[];
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,61 @@
1
+ export type { ProjectMemoryKind } from './project-memory-service.js';
2
+ import type { ProjectMemoryKind } from './project-memory-service.js';
3
+ /**
4
+ * One entry in `.peaks/memory/index.json` after the on-disk `hot[]` +
5
+ * `cold[]` arrays are flattened. Mirrors the field shape that the
6
+ * existing `project-memory-service.ts` writer emits.
7
+ */
8
+ export interface MemoryIndexEntry {
9
+ name: string;
10
+ kind: ProjectMemoryKind;
11
+ description: string;
12
+ sourcePath: string;
13
+ sourceArtifact: string | null;
14
+ updatedAt: string;
15
+ }
16
+ /**
17
+ * The full snapshot of `.peaks/memory/index.json`.
18
+ */
19
+ export interface MemoryIndexSnapshot {
20
+ indexPath: string;
21
+ version: number;
22
+ updatedAt: string;
23
+ entries: MemoryIndexEntry[];
24
+ }
25
+ /**
26
+ * Input to `searchMemory`. `projectRoot` defaults to the resolved
27
+ * peaks project root (CLI resolves this before calling). `query` is
28
+ * required and non-empty.
29
+ */
30
+ export interface MemorySearchInput {
31
+ query: string;
32
+ projectRoot?: string;
33
+ limit?: number;
34
+ kind?: ProjectMemoryKind;
35
+ }
36
+ /**
37
+ * One hit returned by `searchMemory`. Mirrors the entry shape with a
38
+ * normalized score in [0, 1] (top of batch = 1.0) and the char indices
39
+ * in the searchable text that contributed to the match.
40
+ */
41
+ export interface MemorySearchResult {
42
+ name: string;
43
+ kind: ProjectMemoryKind;
44
+ description: string;
45
+ sourcePath: string;
46
+ score: number;
47
+ positions: number[];
48
+ }
49
+ /**
50
+ * Read `.peaks/memory/index.json` and flatten the on-disk `hot[<kind>][]`
51
+ * + `cold[]` shape into a single `entries[]` array. Throws structured
52
+ * errors with stable `code` markers that the CLI converts to the
53
+ * peaks envelope.
54
+ */
55
+ export declare function loadMemoryIndex(projectRoot: string): MemoryIndexSnapshot;
56
+ /**
57
+ * Run the generic fuzzy kernel against the on-disk memory index. The
58
+ * searchable text is `name + " " + description` for each entry (per
59
+ * spec §Component Details).
60
+ */
61
+ export declare function searchMemory(input: MemorySearchInput): MemorySearchResult[];
@@ -0,0 +1,80 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { fuzzyMatchWithKey } from '../fuzzy-matching/fuzzy-match-service.js';
4
+ const DEFAULT_LIMIT = 6;
5
+ /**
6
+ * Read `.peaks/memory/index.json` and flatten the on-disk `hot[<kind>][]`
7
+ * + `cold[]` shape into a single `entries[]` array. Throws structured
8
+ * errors with stable `code` markers that the CLI converts to the
9
+ * peaks envelope.
10
+ */
11
+ export function loadMemoryIndex(projectRoot) {
12
+ const indexPath = join(projectRoot, '.peaks', 'memory', 'index.json');
13
+ if (!existsSync(indexPath)) {
14
+ const err = new Error(`INDEX_MISSING: memory index not found at ${indexPath}`);
15
+ err.code = 'INDEX_MISSING';
16
+ throw err;
17
+ }
18
+ let raw;
19
+ try {
20
+ raw = readFileSync(indexPath, 'utf8');
21
+ }
22
+ catch (cause) {
23
+ const err = new Error(`INDEX_INVALID: failed to read memory index at ${indexPath}: ${cause.message}`);
24
+ err.code = 'INDEX_INVALID';
25
+ throw err;
26
+ }
27
+ let parsed;
28
+ try {
29
+ parsed = JSON.parse(raw);
30
+ }
31
+ catch (cause) {
32
+ const err = new Error(`INDEX_INVALID: malformed memory index at ${indexPath}: ${cause.message}`);
33
+ err.code = 'INDEX_INVALID';
34
+ throw err;
35
+ }
36
+ const index = parsed;
37
+ const hot = index.hot ?? {};
38
+ const flatFromHot = Object.values(hot).flat();
39
+ const flatFromCold = (index.cold ?? []);
40
+ const entries = [...flatFromHot, ...flatFromCold];
41
+ return {
42
+ indexPath,
43
+ version: index.version ?? 1,
44
+ updatedAt: index.updatedAt ?? '',
45
+ entries,
46
+ };
47
+ }
48
+ /**
49
+ * Run the generic fuzzy kernel against the on-disk memory index. The
50
+ * searchable text is `name + " " + description` for each entry (per
51
+ * spec §Component Details).
52
+ */
53
+ export function searchMemory(input) {
54
+ if (input.query === '') {
55
+ const err = new Error('EMPTY_QUERY: searchMemory requires a non-empty query (use `peaks memory index` to list all)');
56
+ err.code = 'EMPTY_QUERY';
57
+ throw err;
58
+ }
59
+ const projectRoot = input.projectRoot ?? process.cwd();
60
+ const limit = input.limit ?? DEFAULT_LIMIT;
61
+ const snapshot = loadMemoryIndex(projectRoot);
62
+ let candidates = snapshot.entries;
63
+ if (input.kind !== undefined) {
64
+ candidates = candidates.filter((e) => e.kind === input.kind);
65
+ }
66
+ // Per spec: searchable text is name + " " + description.
67
+ // The keyFn is invoked once per item per call.
68
+ const matches = fuzzyMatchWithKey(input.query, candidates, { keyFn: (e) => `${e.name} ${e.description}`, limit, caseSensitive: false });
69
+ return matches.map((m) => {
70
+ const entry = m.item;
71
+ return {
72
+ name: entry.name,
73
+ kind: entry.kind,
74
+ description: entry.description,
75
+ sourcePath: entry.sourcePath,
76
+ score: m.score,
77
+ positions: m.positions,
78
+ };
79
+ });
80
+ }
@@ -95,7 +95,6 @@ export const seedCapabilityItems = [
95
95
  capability('agent-browser.browser-agent', 'agent-browser', 'Agent Browser', 'agent', 'browser-agent', ['engineer', 'qa'], 'medium', 'manual-browser-walkthrough', 'Use screenshots and manual test steps if agent browser is unavailable.', 'Agent Browser', '浏览器代理', 'Supports browser-based validation and interaction planning.', '支持基于浏览器的验证和交互规划。'),
96
96
  capability('minimax-skills.worker-guidance', 'minimax-skills', 'MiniMax Worker Guidance', 'skill', 'worker-guidance', ['engineer'], 'medium', 'peaks-worker-contract', 'Use Peaks built-in minimax-worker contract and review handoff.', 'MiniMax Worker Guidance', 'MiniMax Worker 指南', 'Guides MiniMax coding/test worker delegation.', '指导 MiniMax 编码/测试 worker 委托。'),
97
97
  capability('claude-mem.memory-persistence', 'claude-mem', 'Claude Memory Persistence', 'skill', 'memory', ['engineer'], 'medium', 'peaks-txt-context-capsule', 'Use peaks-txt context capsules without storing secrets.', 'Memory Persistence', '记忆持久化', 'Persists reusable context when explicitly approved.', '在明确授权时持久化可复用上下文。'),
98
- capability('shadcn-ui.component-system', 'shadcn-ui', 'shadcn/ui Component System', 'doc', 'ui-components', ['engineer', 'designer'], 'low', 'project-local-ui-patterns', 'Use existing project components and design tokens.', 'Component System Reference', '组件系统参考', 'Provides component and design-system references for UI planning.', '为 UI 规划提供组件和设计系统参考。'),
99
98
  capability('openspec.spec-workflow', 'openspec', 'OpenSpec Workflow', 'workflow', 'spec-workflow', ['product', 'engineer'], 'low', 'peaks-prd-rd-qa-artifacts', 'Use Peaks built-in PRD/RD/QA artifact flow.', 'OpenSpec Workflow', 'OpenSpec 规格流程', 'Supports spec-first product and engineering governance.', '支持规格优先的产品与工程治理。'),
100
99
  capability('gitnexus.repo-intelligence', 'gitnexus', 'GitNexus Repository Intelligence', 'cli', 'repo-intelligence', ['engineer'], 'medium', 'local-repo-scan', 'Use local project scanning through Peaks RD.', 'Repository Intelligence', '仓库智能分析', 'Repository intelligence should be proxied through Peaks before use.', '仓库智能分析应先通过 Peaks 代理边界再使用。'),
101
100
  capability('claude-code-best-practice.workflow-guidance', 'claude-code-best-practice', 'Claude Code Best Practice', 'doc', 'workflow-guidance', ['engineer'], 'low', 'peaks-built-in-rules', 'Use Peaks built-in workflow and review rules.', 'Claude Code Best Practice', 'Claude Code 最佳实践', 'Guidance for Claude Code engineering workflows.', 'Claude Code 工程工作流指导。'),
@@ -33,7 +33,6 @@ export const seedCapabilityLandingMappings = [
33
33
  mapping({ capabilityId: 'agent-browser.browser-agent', sourceId: 'agent-browser', sourceGroup: 'mcp-server', landingKind: 'skill', target: 'peaks-qa', skillName: 'peaks-qa', guidance: 'Use for browser validation; never submit forms or mutate authenticated state without explicit permission.' }),
34
34
  mapping({ capabilityId: 'minimax-skills.worker-guidance', sourceId: 'minimax-skills', sourceGroup: 'mcp-server', landingKind: 'cli', target: 'peaks minimax-worker', commandPreview: 'peaks minimax-worker --json', guidance: 'Use Peaks worker command only after reviewing inputs; add --confirm manually when explicit external-provider approval exists.' }),
35
35
  mapping({ capabilityId: 'claude-mem.memory-persistence', sourceId: 'claude-mem', sourceGroup: 'mcp-server', landingKind: 'skill', target: 'peaks-txt', skillName: 'peaks-txt', guidance: 'Use only with explicit durable-memory consent and never store secrets.' }),
36
- mapping({ capabilityId: 'shadcn-ui.component-system', sourceId: 'shadcn-ui', sourceGroup: 'mcp-server', landingKind: 'skill', target: 'peaks-ui', skillName: 'peaks-ui', guidance: 'Use as a component-system reference, not as an unreviewed generated UI default.' }),
37
36
  mapping({ capabilityId: 'darwin-skill.external-skill', sourceId: 'darwin-skill', sourceGroup: 'mcp-server', landingKind: 'catalog', target: 'external skill catalog', guidance: 'Catalog only until inspected for project fit and safety.' }),
38
37
  mapping({ capabilityId: 'claude-code-best-practice.workflow-guidance', sourceId: 'claude-code-best-practice', sourceGroup: 'mcp-server', landingKind: 'skill', target: 'peaks-rd', skillName: 'peaks-rd', guidance: 'Use as Claude Code workflow reference while preserving Peaks gates.' }),
39
38
  mapping({ capabilityId: 'openspec.spec-workflow', sourceId: 'openspec', sourceGroup: 'mcp-server', landingKind: 'skill', target: 'peaks-prd', skillName: 'peaks-prd', guidance: 'Use for spec-first product and engineering artifact structure.' }),
@@ -17,7 +17,6 @@ export const seedCapabilitySources = [
17
17
  { sourceId: 'agent-browser', sourceType: 'repo', sourceGroup: 'mcp-server', title: 'Agent Browser', url: 'https://github.com/vercel-labs/agent-browser', discoveryStatus: 'indexed', items: ['agent-browser.browser-agent'] },
18
18
  { sourceId: 'minimax-skills', sourceType: 'skills-package', sourceGroup: 'mcp-server', title: 'MiniMax Skills', url: 'https://github.com/MiniMax-AI/skills', discoveryStatus: 'indexed', items: ['minimax-skills.worker-guidance'] },
19
19
  { sourceId: 'claude-mem', sourceType: 'repo', sourceGroup: 'mcp-server', title: 'claude-mem', url: 'https://github.com/thedotmack/claude-mem', discoveryStatus: 'indexed', items: ['claude-mem.memory-persistence'] },
20
- { sourceId: 'shadcn-ui', sourceType: 'repo', sourceGroup: 'mcp-server', title: 'shadcn/ui', url: 'https://github.com/shadcn-ui/ui', discoveryStatus: 'indexed', items: ['shadcn-ui.component-system'] },
21
20
  { sourceId: 'darwin-skill', sourceType: 'skills-package', sourceGroup: 'mcp-server', title: 'darwin-skill', url: 'https://github.com/alchaincyf/darwin-skill', discoveryStatus: 'unscanned', items: ['darwin-skill.external-skill'] },
22
21
  { sourceId: 'claude-code-best-practice', sourceType: 'repo', sourceGroup: 'mcp-server', title: 'Claude Code Best Practice', url: 'https://github.com/shanraisshan/claude-code-best-practice', discoveryStatus: 'indexed', items: ['claude-code-best-practice.workflow-guidance'] },
23
22
  { sourceId: 'openspec', sourceType: 'repo', sourceGroup: 'mcp-server', title: 'OpenSpec', url: 'https://github.com/Fission-AI/OpenSpec', discoveryStatus: 'indexed', items: ['openspec.spec-workflow'] },
@@ -0,0 +1,37 @@
1
+ export type { RetrospectiveType, RetrospectiveOutcome, RetrospectiveEntry } from './retrospective-index.js';
2
+ import type { RetrospectiveType, RetrospectiveOutcome } from './retrospective-index.js';
3
+ /**
4
+ * Input to `searchRetrospective`. `projectRoot` defaults to `process.cwd()`.
5
+ * `query` is required and non-empty. `type` and `outcome` are optional
6
+ * structured filters that compose with AND.
7
+ */
8
+ export interface RetrospectiveSearchInput {
9
+ query: string;
10
+ projectRoot?: string;
11
+ limit?: number;
12
+ type?: RetrospectiveType;
13
+ outcome?: RetrospectiveOutcome;
14
+ }
15
+ /**
16
+ * One hit returned by `searchRetrospective`. The `artifactPaths` are
17
+ * preserved so the LLM can follow up with `peaks retrospective show
18
+ * <id>` (or read the artifact directly).
19
+ */
20
+ export interface RetrospectiveSearchResult {
21
+ id: string;
22
+ sessionId: string;
23
+ type: RetrospectiveType;
24
+ title: string;
25
+ summary: string;
26
+ outcome: RetrospectiveOutcome;
27
+ artifactPaths: string[];
28
+ score: number;
29
+ positions: number[];
30
+ }
31
+ /**
32
+ * Run the generic fuzzy kernel against the on-disk retrospective index.
33
+ * Searchable text is `title + " " + summary` per spec §Component Details.
34
+ * `--type` and `--outcome` filters compose with AND before the kernel
35
+ * runs (cheaper to filter, then fuzzy on a smaller set).
36
+ */
37
+ export declare function searchRetrospective(input: RetrospectiveSearchInput): RetrospectiveSearchResult[];