peaks-cli 1.3.8 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cli/commands/core-artifact-commands.js +27 -0
- package/dist/src/cli/commands/project-commands.js +58 -1
- package/dist/src/cli/commands/request-commands.js +93 -3
- package/dist/src/cli/commands/retrospective-commands.d.ts +3 -0
- package/dist/src/cli/commands/retrospective-commands.js +113 -0
- package/dist/src/cli/commands/skill-scope-commands.d.ts +49 -0
- package/dist/src/cli/commands/skill-scope-commands.js +305 -0
- package/dist/src/cli/commands/workflow-commands.js +1 -1
- package/dist/src/cli/commands/workflow-plan-commands.d.ts +39 -0
- package/dist/src/cli/commands/workflow-plan-commands.js +163 -0
- package/dist/src/cli/program.js +8 -0
- package/dist/src/services/doctor/doctor-service.d.ts +40 -0
- package/dist/src/services/doctor/doctor-service.js +160 -0
- package/dist/src/services/hooks/presence-marker-detector.d.ts +16 -0
- package/dist/src/services/hooks/presence-marker-detector.js +105 -0
- package/dist/src/services/memory/project-memory-service.d.ts +19 -0
- package/dist/src/services/memory/project-memory-service.js +33 -0
- package/dist/src/services/retrospective/migrate-from-md.d.ts +37 -0
- package/dist/src/services/retrospective/migrate-from-md.js +528 -0
- package/dist/src/services/retrospective/retrospective-index.d.ts +37 -0
- package/dist/src/services/retrospective/retrospective-index.js +110 -0
- package/dist/src/services/retrospective/retrospective-show.d.ts +40 -0
- package/dist/src/services/retrospective/retrospective-show.js +109 -0
- package/dist/src/services/skill-scope/adapters/_stub-helper.d.ts +39 -0
- package/dist/src/services/skill-scope/adapters/_stub-helper.js +98 -0
- package/dist/src/services/skill-scope/adapters/claude-code.d.ts +59 -0
- package/dist/src/services/skill-scope/adapters/claude-code.js +304 -0
- package/dist/src/services/skill-scope/adapters/codex.d.ts +2 -0
- package/dist/src/services/skill-scope/adapters/codex.js +12 -0
- package/dist/src/services/skill-scope/adapters/cursor.d.ts +2 -0
- package/dist/src/services/skill-scope/adapters/cursor.js +13 -0
- package/dist/src/services/skill-scope/adapters/qoder.d.ts +2 -0
- package/dist/src/services/skill-scope/adapters/qoder.js +13 -0
- package/dist/src/services/skill-scope/adapters/tongyi.d.ts +2 -0
- package/dist/src/services/skill-scope/adapters/tongyi.js +13 -0
- package/dist/src/services/skill-scope/adapters/trae.d.ts +2 -0
- package/dist/src/services/skill-scope/adapters/trae.js +12 -0
- package/dist/src/services/skill-scope/detect.d.ts +75 -0
- package/dist/src/services/skill-scope/detect.js +480 -0
- package/dist/src/services/skill-scope/registry.d.ts +41 -0
- package/dist/src/services/skill-scope/registry.js +83 -0
- package/dist/src/services/skill-scope/source-of-truth.d.ts +44 -0
- package/dist/src/services/skill-scope/source-of-truth.js +118 -0
- package/dist/src/services/skill-scope/types.d.ts +176 -0
- package/dist/src/services/skill-scope/types.js +74 -0
- package/dist/src/services/standards/migrate-service.d.ts +63 -0
- package/dist/src/services/standards/migrate-service.js +193 -0
- package/dist/src/services/standards/project-standards-service.js +1 -23
- package/dist/src/services/workflow/artifact-paths.d.ts +59 -0
- package/dist/src/services/workflow/artifact-paths.js +127 -0
- package/dist/src/services/workflow/pipeline-verify-service.d.ts +6 -0
- package/dist/src/services/workflow/pipeline-verify-service.js +49 -4
- package/dist/src/services/workflow/plan-reader.d.ts +29 -0
- package/dist/src/services/workflow/plan-reader.js +158 -0
- package/dist/src/services/workflow/plan-refresher.d.ts +32 -0
- package/dist/src/services/workflow/plan-refresher.js +353 -0
- package/dist/src/services/workflow/plan-trigger-detector.d.ts +55 -0
- package/dist/src/services/workflow/plan-trigger-detector.js +142 -0
- package/dist/src/shared/format-md-compact.d.ts +32 -0
- package/dist/src/shared/format-md-compact.js +297 -0
- package/dist/src/shared/stale-policy.d.ts +67 -0
- package/dist/src/shared/stale-policy.js +85 -0
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +3 -2
- package/schemas/doctor-report.schema.json +2 -2
- package/skills/peaks-qa/SKILL.md +103 -507
- package/skills/peaks-qa/references/artifact-per-request.md +7 -79
- package/skills/peaks-qa/references/browser-validation-contracts.md +51 -0
- package/skills/peaks-qa/references/codegraph-regression-focus.md +5 -0
- package/skills/peaks-qa/references/external-capability-guidance.md +9 -0
- package/skills/peaks-qa/references/qa-compact-handoff.md +3 -0
- package/skills/peaks-qa/references/qa-context-governance.md +24 -0
- package/skills/peaks-qa/references/qa-fanout-contract.md +8 -0
- package/skills/peaks-qa/references/qa-gstack-integration.md +7 -0
- package/skills/peaks-qa/references/qa-local-artifacts.md +3 -0
- package/skills/peaks-qa/references/qa-matt-pocock-integration.md +9 -0
- package/skills/peaks-qa/references/qa-perf-test-plan.md +67 -0
- package/skills/peaks-qa/references/qa-refactor-role.md +3 -0
- package/skills/peaks-qa/references/qa-runbook.md +74 -0
- package/skills/peaks-qa/references/qa-security-test-plan.md +73 -0
- package/skills/peaks-qa/references/qa-skill-presence.md +22 -0
- package/skills/peaks-qa/references/qa-standards-preflight.md +8 -0
- package/skills/peaks-qa/references/qa-sub-agent-dispatch.md +38 -0
- package/skills/peaks-qa/references/qa-transition-gates.md +83 -0
- package/skills/peaks-qa/references/requirement-boundary-recheck.md +9 -0
- package/skills/peaks-qa/references/test-case-generation.md +27 -0
- package/skills/peaks-qa/references/test-report-output.md +14 -0
- package/skills/peaks-rd/SKILL.md +85 -612
- package/skills/peaks-rd/references/artifact-and-standards-output.md +9 -0
- package/skills/peaks-rd/references/artifact-per-request.md +20 -0
- package/skills/peaks-rd/references/browser-self-test-contracts.md +29 -0
- package/skills/peaks-rd/references/codegraph-project-analysis.md +5 -0
- package/skills/peaks-rd/references/compact-handoff.md +3 -0
- package/skills/peaks-rd/references/external-references.md +11 -0
- package/skills/peaks-rd/references/frontend-project-generation.md +11 -0
- package/skills/peaks-rd/references/library-version-awareness.md +30 -0
- package/skills/peaks-rd/references/mandatory-perf-baseline.md +42 -0
- package/skills/peaks-rd/references/mandatory-tech-doc.md +18 -0
- package/skills/peaks-rd/references/matt-pocock-integration.md +11 -0
- package/skills/peaks-rd/references/mock-data-placement.md +40 -0
- package/skills/peaks-rd/references/parallel-review-fanout.md +81 -0
- package/skills/peaks-rd/references/rd-context-governance.md +36 -0
- package/skills/peaks-rd/references/rd-gstack-integration.md +16 -0
- package/skills/peaks-rd/references/rd-runbook.md +125 -0
- package/skills/peaks-rd/references/rd-standards-preflight.md +8 -0
- package/skills/peaks-rd/references/rd-sub-agent-dispatch.md +39 -0
- package/skills/peaks-rd/references/rd-transition-gates.md +1 -1
- package/skills/peaks-rd/references/skill-presence-and-title.md +22 -0
- package/skills/peaks-solo/SKILL.md +87 -595
- package/skills/peaks-solo/references/anchoring-and-session-info.md +25 -0
- package/skills/peaks-solo/references/boundaries.md +21 -0
- package/skills/peaks-solo/references/codegraph-orchestration.md +5 -0
- package/skills/peaks-solo/references/completion-handoff.md +16 -0
- package/skills/peaks-solo/references/context-governance.md +51 -0
- package/skills/peaks-solo/references/external-references.md +17 -0
- package/skills/peaks-solo/references/frontend-only-mode.md +14 -0
- package/skills/peaks-solo/references/gstack-integration.md +7 -0
- package/skills/peaks-solo/references/local-artifact-workspace.md +79 -0
- package/skills/peaks-solo/references/micro-cycle.md +68 -0
- package/skills/peaks-solo/references/mode-selection.md +21 -0
- package/skills/peaks-solo/references/openspec-workflow.md +43 -0
- package/skills/peaks-solo/references/project-memory-loading.md +17 -0
- package/skills/peaks-solo/references/quality-gate-cheatsheet.md +13 -0
- package/skills/peaks-solo/references/resume-detection.md +63 -0
- package/skills/peaks-solo/references/runbook.md +1 -1
- package/skills/peaks-solo/references/skill-presence-and-title.md +31 -0
- package/skills/peaks-solo/references/standards-preflight.md +23 -0
- package/skills/peaks-solo/references/sub-agent-dispatch.md +46 -0
- package/skills/peaks-solo/references/swarm-dispatch-contract.md +56 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// TODO(slice-025.3-cursor): research Cursor's per-project skill scoping
|
|
2
|
+
// config format. Cursor uses `.cursor/` for project-local config; the
|
|
3
|
+
// skill-scope hook may live there or in `.cursor/rules/`.
|
|
4
|
+
//
|
|
5
|
+
// Until the real format is known, this stub:
|
|
6
|
+
// 1. Writes `.peaks/scope/cursor-skills.json` (source-of-truth) on every
|
|
7
|
+
// `applyScope` so the user's intent is captured on disk.
|
|
8
|
+
// 2. Returns NOT_SUPPORTED with a clear message pointing at this slice.
|
|
9
|
+
//
|
|
10
|
+
// When implementing, replace the makeStubAdapter call with the real
|
|
11
|
+
// CursorSkillScope class.
|
|
12
|
+
import { makeStubAdapter } from './_stub-helper.js';
|
|
13
|
+
export const CURSOR_SKILL_SCOPE = makeStubAdapter('cursor', 'slice-025.3-cursor', 'Cursor');
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// TODO(slice-025.5-qoder): research Qoder's per-project skill scoping
|
|
2
|
+
// config format. Qoder is an AI IDE by Alibaba; per-project config dir
|
|
3
|
+
// is unconfirmed at slice 025.1.
|
|
4
|
+
//
|
|
5
|
+
// Until the real format is known, this stub:
|
|
6
|
+
// 1. Writes `.peaks/scope/qoder-skills.json` (source-of-truth) on every
|
|
7
|
+
// `applyScope` so the user's intent is captured on disk.
|
|
8
|
+
// 2. Returns NOT_SUPPORTED with a clear message pointing at this slice.
|
|
9
|
+
//
|
|
10
|
+
// When implementing, replace the makeStubAdapter call with the real
|
|
11
|
+
// QoderSkillScope class.
|
|
12
|
+
import { makeStubAdapter } from './_stub-helper.js';
|
|
13
|
+
export const QODER_SKILL_SCOPE = makeStubAdapter('qoder', 'slice-025.5-qoder', 'Qoder');
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// TODO(slice-025.6-tongyi): research Tongyi Lingma's per-project skill
|
|
2
|
+
// scoping config format. Tongyi Lingma is an AI IDE by Alibaba; per-
|
|
3
|
+
// project config dir is unconfirmed at slice 025.1.
|
|
4
|
+
//
|
|
5
|
+
// Until the real format is known, this stub:
|
|
6
|
+
// 1. Writes `.peaks/scope/tongyi-lingma-skills.json` (source-of-truth)
|
|
7
|
+
// on every `applyScope` so the user's intent is captured on disk.
|
|
8
|
+
// 2. Returns NOT_SUPPORTED with a clear message pointing at this slice.
|
|
9
|
+
//
|
|
10
|
+
// When implementing, replace the makeStubAdapter call with the real
|
|
11
|
+
// TongyiSkillScope class.
|
|
12
|
+
import { makeStubAdapter } from './_stub-helper.js';
|
|
13
|
+
export const TONGYI_SKILL_SCOPE = makeStubAdapter('tongyi-lingma', 'slice-025.6-tongyi', 'Tongyi Lingma');
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// TODO(slice-025.2-trae): research Trae's per-project skill scoping config
|
|
2
|
+
// format. Likely candidates: `.trae/skills.json` or `.trae/settings.json`.
|
|
3
|
+
//
|
|
4
|
+
// Until the real format is known, this stub:
|
|
5
|
+
// 1. Writes `.peaks/scope/trae-skills.json` (source-of-truth) on every
|
|
6
|
+
// `applyScope` so the user's intent is captured on disk.
|
|
7
|
+
// 2. Returns NOT_SUPPORTED with a clear message pointing at this slice.
|
|
8
|
+
//
|
|
9
|
+
// When implementing, replace the makeStubAdapter call with the real
|
|
10
|
+
// TraeSkillScope class.
|
|
11
|
+
import { makeStubAdapter } from './_stub-helper.js';
|
|
12
|
+
export const TRAE_SKILL_SCOPE = makeStubAdapter('trae', 'slice-025.2-trae', 'Trae IDE');
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection algorithm for `peaks skill scope`.
|
|
3
|
+
*
|
|
4
|
+
* Pure function: given a project root and the installed-skills path, the
|
|
5
|
+
* algorithm produces a `DetectResult` (signals + per-skill classification
|
|
6
|
+
* + counts). No filesystem writes, no randomness, no time-of-day. AC11.
|
|
7
|
+
*
|
|
8
|
+
* Three layers:
|
|
9
|
+
* 1. `extractProjectSignals(projectRoot)` — read package.json + tsconfig +
|
|
10
|
+
* file tree (top-50 extensions).
|
|
11
|
+
* 2. `classifySkill(skill, signals, hardcodedRules)` — keyword matching
|
|
12
|
+
* against the skill's SKILL.md description.
|
|
13
|
+
* 3. `detectSkillScope({ projectRoot, installedSkillsPath })` — top-level
|
|
14
|
+
* orchestrator that returns the JSON envelope (AC1).
|
|
15
|
+
*/
|
|
16
|
+
import type { ProjectSignals, SkillKind, SkillScopeCounts, SkillScopeRecord } from './types.js';
|
|
17
|
+
/**
|
|
18
|
+
* Walk `src/` (recursively) AND the project root, collecting the top-50
|
|
19
|
+
* unique file extensions. Sorted lexicographically.
|
|
20
|
+
*/
|
|
21
|
+
export declare function scanFileTree(projectRoot: string, maxExtensions?: number): string[];
|
|
22
|
+
/**
|
|
23
|
+
* Build the `ProjectSignals` object from the project root.
|
|
24
|
+
*/
|
|
25
|
+
export declare function extractProjectSignals(projectRoot: string): Promise<ProjectSignals>;
|
|
26
|
+
export interface InstalledSkill {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly description: string;
|
|
29
|
+
readonly skillPath: string;
|
|
30
|
+
}
|
|
31
|
+
interface HardcodedRules {
|
|
32
|
+
readonly alwaysRelevant: ReadonlySet<string>;
|
|
33
|
+
readonly nonTsPrefixes: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Infer the `SkillKind` for a skill by name. Used for the JSON envelope.
|
|
37
|
+
*/
|
|
38
|
+
export declare function inferSkillKind(name: string, alwaysRelevant: ReadonlySet<string>): SkillKind;
|
|
39
|
+
/**
|
|
40
|
+
* Classify a single skill given the project signals. Returns the relevance
|
|
41
|
+
* + a list of human-readable reasons (stable for fixtures, so unit tests
|
|
42
|
+
* can assert exact strings).
|
|
43
|
+
*/
|
|
44
|
+
export declare function classifySkill(skill: InstalledSkill, signals: ProjectSignals, rules: HardcodedRules): SkillScopeRecord;
|
|
45
|
+
export interface DetectInput {
|
|
46
|
+
readonly projectRoot: string;
|
|
47
|
+
readonly installedSkillsPath?: string;
|
|
48
|
+
readonly detectedIde?: string | null;
|
|
49
|
+
}
|
|
50
|
+
export interface DetectResult {
|
|
51
|
+
readonly detectedIde: string | null;
|
|
52
|
+
readonly projectSignals: ProjectSignals;
|
|
53
|
+
readonly skills: readonly SkillScopeRecord[];
|
|
54
|
+
readonly counts: SkillScopeCounts;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Discover the installed skills under `installedSkillsPath` (default:
|
|
58
|
+
* `~/.claude/skills`). Each subdir containing a SKILL.md counts as an
|
|
59
|
+
* installed skill.
|
|
60
|
+
*/
|
|
61
|
+
export declare function listInstalledSkills(installedSkillsPath: string): Promise<InstalledSkill[]>;
|
|
62
|
+
/**
|
|
63
|
+
* The default installed-skills path: `~/.claude/skills`. Resolved at call
|
|
64
|
+
* time so the orchestrator stays pure-ish (no module-level side effects).
|
|
65
|
+
*/
|
|
66
|
+
export declare function defaultInstalledSkillsPath(): string;
|
|
67
|
+
/**
|
|
68
|
+
* Top-level orchestrator. Reads package.json + tsconfig + file tree,
|
|
69
|
+
* discovers installed skills, classifies each one, returns the JSON
|
|
70
|
+
* envelope. Idempotent: same input → same output. No filesystem writes.
|
|
71
|
+
*/
|
|
72
|
+
export declare function detectSkillScope(input: DetectInput): Promise<DetectResult>;
|
|
73
|
+
/** Compute a stable summary hash (used by tests to assert no time-dependent fields). */
|
|
74
|
+
export declare function detectSummary(result: DetectResult): string;
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection algorithm for `peaks skill scope`.
|
|
3
|
+
*
|
|
4
|
+
* Pure function: given a project root and the installed-skills path, the
|
|
5
|
+
* algorithm produces a `DetectResult` (signals + per-skill classification
|
|
6
|
+
* + counts). No filesystem writes, no randomness, no time-of-day. AC11.
|
|
7
|
+
*
|
|
8
|
+
* Three layers:
|
|
9
|
+
* 1. `extractProjectSignals(projectRoot)` — read package.json + tsconfig +
|
|
10
|
+
* file tree (top-50 extensions).
|
|
11
|
+
* 2. `classifySkill(skill, signals, hardcodedRules)` — keyword matching
|
|
12
|
+
* against the skill's SKILL.md description.
|
|
13
|
+
* 3. `detectSkillScope({ projectRoot, installedSkillsPath })` — top-level
|
|
14
|
+
* orchestrator that returns the JSON envelope (AC1).
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
17
|
+
import { readFile } from 'node:fs/promises';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { ALWAYS_RELEVANT_SKILLS, NON_TS_SKILL_PREFIXES, TRACKED_EXTENSIONS, } from './types.js';
|
|
20
|
+
function hasAnyDep(pkg, names) {
|
|
21
|
+
const all = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
22
|
+
return names.some((name) => Object.prototype.hasOwnProperty.call(all, name));
|
|
23
|
+
}
|
|
24
|
+
function parseNodeEngineMajor(enginesNode) {
|
|
25
|
+
if (enginesNode === undefined)
|
|
26
|
+
return null;
|
|
27
|
+
// Match patterns like '>=20', '^20.0.0', '>=20.0.0 <21.0.0'
|
|
28
|
+
const match = enginesNode.match(/(\d+)/);
|
|
29
|
+
return match === null ? null : Number(match[1]);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read and parse a JSON file. Returns null on parse error or missing file.
|
|
33
|
+
*/
|
|
34
|
+
async function readJson(path) {
|
|
35
|
+
if (!existsSync(path))
|
|
36
|
+
return null;
|
|
37
|
+
try {
|
|
38
|
+
const raw = await readFile(path, 'utf8');
|
|
39
|
+
return JSON.parse(raw);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function asPackageJson(value) {
|
|
46
|
+
if (value === null || typeof value !== 'object')
|
|
47
|
+
return null;
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function asTsConfig(value) {
|
|
51
|
+
if (value === null || typeof value !== 'object')
|
|
52
|
+
return null;
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Walk `src/` (recursively) AND the project root, collecting the top-50
|
|
57
|
+
* unique file extensions. Sorted lexicographically.
|
|
58
|
+
*/
|
|
59
|
+
export function scanFileTree(projectRoot, maxExtensions = 50) {
|
|
60
|
+
const roots = [];
|
|
61
|
+
if (existsSync(join(projectRoot, 'src')))
|
|
62
|
+
roots.push(join(projectRoot, 'src'));
|
|
63
|
+
roots.push(projectRoot);
|
|
64
|
+
const exts = new Set();
|
|
65
|
+
// Bound the walk: at most 2000 files, 5 levels deep.
|
|
66
|
+
let visited = 0;
|
|
67
|
+
const MAX_FILES = 2000;
|
|
68
|
+
const MAX_DEPTH = 5;
|
|
69
|
+
for (const root of roots) {
|
|
70
|
+
const stack = [root];
|
|
71
|
+
while (stack.length > 0 && visited < MAX_FILES) {
|
|
72
|
+
const dir = stack.pop();
|
|
73
|
+
if (!existsSync(dir))
|
|
74
|
+
continue;
|
|
75
|
+
let entries;
|
|
76
|
+
try {
|
|
77
|
+
entries = readdirSync(dir, { withFileTypes: true, encoding: 'utf8' });
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const name = entry.name;
|
|
84
|
+
if (typeof name !== 'string')
|
|
85
|
+
continue;
|
|
86
|
+
const full = join(dir, name);
|
|
87
|
+
// Skip hidden dirs (e.g. node_modules, .git) and the fixture `skills/` dir.
|
|
88
|
+
if (name === 'node_modules' || name === '.git' || name === 'skills' || name === 'dist')
|
|
89
|
+
continue;
|
|
90
|
+
if (entry.isDirectory()) {
|
|
91
|
+
const depth = full.split(/[/\\]/).length - projectRoot.split(/[/\\]/).length;
|
|
92
|
+
if (depth < MAX_DEPTH)
|
|
93
|
+
stack.push(full);
|
|
94
|
+
}
|
|
95
|
+
else if (entry.isFile()) {
|
|
96
|
+
visited += 1;
|
|
97
|
+
if (visited >= MAX_FILES)
|
|
98
|
+
break;
|
|
99
|
+
const dot = name.lastIndexOf('.');
|
|
100
|
+
if (dot < 0)
|
|
101
|
+
continue;
|
|
102
|
+
const ext = name.slice(dot).toLowerCase();
|
|
103
|
+
exts.add(ext);
|
|
104
|
+
if (exts.size >= maxExtensions)
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return [...exts].sort().slice(0, maxExtensions);
|
|
111
|
+
}
|
|
112
|
+
function hasExt(exts, ext) {
|
|
113
|
+
return exts.includes(ext);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Build the `ProjectSignals` object from the project root.
|
|
117
|
+
*/
|
|
118
|
+
export async function extractProjectSignals(projectRoot) {
|
|
119
|
+
const pkgRaw = await readJson(join(projectRoot, 'package.json'));
|
|
120
|
+
const pkg = asPackageJson(pkgRaw);
|
|
121
|
+
const hasPackageJson = pkg !== null;
|
|
122
|
+
const isTypeScript = hasPackageJson &&
|
|
123
|
+
(hasAnyDep(pkg, ['typescript', 'tsx', '@types/node']) ||
|
|
124
|
+
existsSync(join(projectRoot, 'tsconfig.json')));
|
|
125
|
+
const tsRaw = await readJson(join(projectRoot, 'tsconfig.json'));
|
|
126
|
+
const tsConfig = asTsConfig(tsRaw);
|
|
127
|
+
const isTypeScriptESM = (pkg?.type === 'module') ||
|
|
128
|
+
(tsConfig?.compilerOptions?.module !== undefined &&
|
|
129
|
+
['ESNext', 'NodeNext', 'ES2022'].includes(tsConfig.compilerOptions.module));
|
|
130
|
+
const isReact = pkg !== null && hasAnyDep(pkg, ['react', 'react-dom', 'preact']);
|
|
131
|
+
const isVue = pkg !== null && hasAnyDep(pkg, ['vue']);
|
|
132
|
+
const isSvelte = pkg !== null && hasAnyDep(pkg, ['svelte']);
|
|
133
|
+
const isNext = pkg !== null && hasAnyDep(pkg, ['next']);
|
|
134
|
+
const isNestJS = pkg !== null && hasAnyDep(pkg, ['@nestjs/core', '@nestjs/common']);
|
|
135
|
+
const isExpress = pkg !== null && hasAnyDep(pkg, ['express']);
|
|
136
|
+
const isFastify = pkg !== null && hasAnyDep(pkg, ['fastify']);
|
|
137
|
+
const isPostgres = pkg !== null && (hasAnyDep(pkg, ['pg', 'postgres', 'postgresql', 'prisma', '@prisma/client']));
|
|
138
|
+
const isMysql = pkg !== null && hasAnyDep(pkg, ['mysql', 'mysql2']);
|
|
139
|
+
const isMongo = pkg !== null && hasAnyDep(pkg, ['mongodb', 'mongoose']);
|
|
140
|
+
const isRedis = pkg !== null && hasAnyDep(pkg, ['redis', 'ioredis']);
|
|
141
|
+
const isDocker = existsSync(join(projectRoot, 'Dockerfile')) ||
|
|
142
|
+
existsSync(join(projectRoot, 'docker-compose.yml')) ||
|
|
143
|
+
existsSync(join(projectRoot, 'docker-compose.yaml'));
|
|
144
|
+
const isK8s = existsSync(join(projectRoot, 'k8s')) ||
|
|
145
|
+
existsSync(join(projectRoot, 'kubernetes')) ||
|
|
146
|
+
existsSync(join(projectRoot, 'deployment.yaml')) ||
|
|
147
|
+
existsSync(join(projectRoot, 'deployment.yml'));
|
|
148
|
+
const isCommander = pkg !== null && hasAnyDep(pkg, ['commander']);
|
|
149
|
+
// Detect Python projects (requirements.txt, pyproject.toml, setup.py, .py presence).
|
|
150
|
+
const isPython = !hasPackageJson ||
|
|
151
|
+
existsSync(join(projectRoot, 'requirements.txt')) ||
|
|
152
|
+
existsSync(join(projectRoot, 'pyproject.toml')) ||
|
|
153
|
+
existsSync(join(projectRoot, 'setup.py'));
|
|
154
|
+
const isCodegraph = pkg !== null && hasAnyDep(pkg, ['@colbymchenry/codegraph']);
|
|
155
|
+
const isHeadroom = pkg !== null && (hasAnyDep(pkg, ['headroom-ai']) ||
|
|
156
|
+
Object.keys({ ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) }).some((k) => k.startsWith('@headroom/')));
|
|
157
|
+
const nodeEngineMajor = parseNodeEngineMajor(pkg?.engines?.node);
|
|
158
|
+
const topExtensions = scanFileTree(projectRoot);
|
|
159
|
+
// Build the per-extension presence flag map.
|
|
160
|
+
const hasFileExtension = {};
|
|
161
|
+
for (const ext of TRACKED_EXTENSIONS) {
|
|
162
|
+
hasFileExtension[ext.slice(1)] = hasExt(topExtensions, ext);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
hasPackageJson,
|
|
166
|
+
isTypeScript,
|
|
167
|
+
isTypeScriptESM,
|
|
168
|
+
isReact,
|
|
169
|
+
isVue,
|
|
170
|
+
isSvelte,
|
|
171
|
+
isNext,
|
|
172
|
+
isNestJS,
|
|
173
|
+
isExpress,
|
|
174
|
+
isFastify,
|
|
175
|
+
isPostgres,
|
|
176
|
+
isMysql,
|
|
177
|
+
isMongo,
|
|
178
|
+
isRedis,
|
|
179
|
+
isDocker,
|
|
180
|
+
isK8s,
|
|
181
|
+
isCommander,
|
|
182
|
+
isCodegraph,
|
|
183
|
+
isHeadroom,
|
|
184
|
+
isPython,
|
|
185
|
+
nodeEngineMajor,
|
|
186
|
+
topExtensions,
|
|
187
|
+
hasFileExtension,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Infer the `SkillKind` for a skill by name. Used for the JSON envelope.
|
|
192
|
+
*/
|
|
193
|
+
export function inferSkillKind(name, alwaysRelevant) {
|
|
194
|
+
if (alwaysRelevant.has(name) && name.startsWith('peaks-'))
|
|
195
|
+
return 'peaks-family';
|
|
196
|
+
if (alwaysRelevant.has(name))
|
|
197
|
+
return 'generic-ai';
|
|
198
|
+
if (NON_TS_SKILL_PREFIXES.some((p) => name.startsWith(p)))
|
|
199
|
+
return 'language-specific';
|
|
200
|
+
return 'other';
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Classify a single skill given the project signals. Returns the relevance
|
|
204
|
+
* + a list of human-readable reasons (stable for fixtures, so unit tests
|
|
205
|
+
* can assert exact strings).
|
|
206
|
+
*/
|
|
207
|
+
export function classifySkill(skill, signals, rules) {
|
|
208
|
+
const reasons = [];
|
|
209
|
+
// 1. Hard-coded allowlist always wins.
|
|
210
|
+
if (rules.alwaysRelevant.has(skill.name)) {
|
|
211
|
+
reasons.push('hard-coded always-relevant');
|
|
212
|
+
return {
|
|
213
|
+
name: skill.name,
|
|
214
|
+
kind: inferSkillKind(skill.name, rules.alwaysRelevant),
|
|
215
|
+
relevance: 'relevant',
|
|
216
|
+
reasons,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
// 2. Non-TS prefix → irrelevant when the project is TS.
|
|
220
|
+
if (rules.nonTsPrefixes.some((prefix) => skill.name.startsWith(prefix))) {
|
|
221
|
+
if (signals.isTypeScript && !isNonTsProject(signals)) {
|
|
222
|
+
reasons.push('non-TS skill prefix; project is TS');
|
|
223
|
+
return {
|
|
224
|
+
name: skill.name,
|
|
225
|
+
kind: 'language-specific',
|
|
226
|
+
relevance: 'irrelevant',
|
|
227
|
+
reasons,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// 3. Keyword matching against the description (strong + weak hits).
|
|
232
|
+
const desc = skill.description.toLowerCase();
|
|
233
|
+
// Special-case: when the project is a non-TS project (Python, etc.),
|
|
234
|
+
// language-specific skills with matching keywords should be relevant.
|
|
235
|
+
if (isNonTsProject(signals)) {
|
|
236
|
+
const langMatch = languageKeywordMatch(desc);
|
|
237
|
+
if (langMatch !== null) {
|
|
238
|
+
reasons.push(`${langMatch} keyword + non-TS project`);
|
|
239
|
+
return {
|
|
240
|
+
name: skill.name,
|
|
241
|
+
kind: inferSkillKind(skill.name, rules.alwaysRelevant),
|
|
242
|
+
relevance: 'relevant',
|
|
243
|
+
reasons,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const strong = strongMatches(desc, signals);
|
|
248
|
+
const weak = weakMatches(desc, signals);
|
|
249
|
+
if (strong.length > 0) {
|
|
250
|
+
reasons.push(...strong);
|
|
251
|
+
return {
|
|
252
|
+
name: skill.name,
|
|
253
|
+
kind: inferSkillKind(skill.name, rules.alwaysRelevant),
|
|
254
|
+
relevance: 'relevant',
|
|
255
|
+
reasons,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (weak.length > 0) {
|
|
259
|
+
reasons.push(...weak);
|
|
260
|
+
return {
|
|
261
|
+
name: skill.name,
|
|
262
|
+
kind: inferSkillKind(skill.name, rules.alwaysRelevant),
|
|
263
|
+
relevance: 'borderline',
|
|
264
|
+
reasons,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
name: skill.name,
|
|
269
|
+
kind: inferSkillKind(skill.name, rules.alwaysRelevant),
|
|
270
|
+
relevance: 'irrelevant',
|
|
271
|
+
reasons: ['no project-signal match'],
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Strong matches: keyword in description that maps to a confirmed project signal.
|
|
276
|
+
*/
|
|
277
|
+
function strongMatches(description, signals) {
|
|
278
|
+
const matches = [];
|
|
279
|
+
if (signals.isReact && /\breact\b/.test(description))
|
|
280
|
+
matches.push('react project + react skill');
|
|
281
|
+
if (signals.isVue && /\bvue\b/.test(description))
|
|
282
|
+
matches.push('vue project + vue skill');
|
|
283
|
+
if (signals.isSvelte && /\bsvelte\b/.test(description))
|
|
284
|
+
matches.push('svelte project + svelte skill');
|
|
285
|
+
if (signals.isNext && /\bnext\.?js\b|\bnextjs\b/.test(description))
|
|
286
|
+
matches.push('next project + nextjs skill');
|
|
287
|
+
if (signals.isNestJS && /\bnest\.?js\b|\bnestjs\b/.test(description))
|
|
288
|
+
matches.push('nestjs project + nestjs skill');
|
|
289
|
+
if (signals.isExpress && /\bexpress\b/.test(description))
|
|
290
|
+
matches.push('express project + express skill');
|
|
291
|
+
if (signals.isFastify && /\bfastify\b/.test(description))
|
|
292
|
+
matches.push('fastify project + fastify skill');
|
|
293
|
+
if (signals.isPostgres && /\bpostgres|\bpostgresql\b/.test(description))
|
|
294
|
+
matches.push('postgres project + postgres skill');
|
|
295
|
+
if (signals.isMysql && /\bmysql\b/.test(description))
|
|
296
|
+
matches.push('mysql project + mysql skill');
|
|
297
|
+
if (signals.isMongo && /\bmongo(?:db)?\b/.test(description))
|
|
298
|
+
matches.push('mongo project + mongo skill');
|
|
299
|
+
if (signals.isRedis && /\bredis\b/.test(description))
|
|
300
|
+
matches.push('redis project + redis skill');
|
|
301
|
+
if (signals.isDocker && /\bdocker\b/.test(description))
|
|
302
|
+
matches.push('docker project + docker skill');
|
|
303
|
+
if (signals.isK8s && /\bkubernetes\b|\bk8s\b/.test(description))
|
|
304
|
+
matches.push('k8s project + k8s skill');
|
|
305
|
+
if (signals.isCommander && /\bcommander\b|\bcli\b/.test(description))
|
|
306
|
+
matches.push('cli project + cli skill');
|
|
307
|
+
if (/\btdd\b|\btest-driven\b/.test(description))
|
|
308
|
+
matches.push('tdd keyword (always relevant)');
|
|
309
|
+
if (/\brefactor\b/.test(description))
|
|
310
|
+
matches.push('refactor keyword (always relevant)');
|
|
311
|
+
return matches;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Weak matches: keyword that's a hint but not a confirmed signal.
|
|
315
|
+
*/
|
|
316
|
+
function weakMatches(description, signals) {
|
|
317
|
+
const matches = [];
|
|
318
|
+
if (/\bfrontend\b/.test(description) &&
|
|
319
|
+
(signals.isReact || signals.isVue || signals.isSvelte || signals.isNext)) {
|
|
320
|
+
matches.push('frontend keyword + frontend project');
|
|
321
|
+
}
|
|
322
|
+
if (/\bbackend\b/.test(description) &&
|
|
323
|
+
(signals.isNestJS || signals.isExpress || signals.isFastify)) {
|
|
324
|
+
matches.push('backend keyword + backend project');
|
|
325
|
+
}
|
|
326
|
+
if (/\bdatabase\b/.test(description) &&
|
|
327
|
+
(signals.isPostgres || signals.isMysql || signals.isMongo || signals.isRedis)) {
|
|
328
|
+
matches.push('database keyword + db project');
|
|
329
|
+
}
|
|
330
|
+
return matches;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Map a skill description to a non-TS language when the description
|
|
334
|
+
* explicitly mentions that language. Returns null when there's no match.
|
|
335
|
+
*/
|
|
336
|
+
function languageKeywordMatch(description) {
|
|
337
|
+
if (/\bpython\b/.test(description))
|
|
338
|
+
return 'python';
|
|
339
|
+
if (/\bkotlin\b/.test(description))
|
|
340
|
+
return 'kotlin';
|
|
341
|
+
if (/\bjava\b/.test(description))
|
|
342
|
+
return 'java';
|
|
343
|
+
if (/\brust\b/.test(description))
|
|
344
|
+
return 'rust';
|
|
345
|
+
if (/\bgo\b|\bgolang\b/.test(description))
|
|
346
|
+
return 'go';
|
|
347
|
+
if (/\bruby\b/.test(description))
|
|
348
|
+
return 'ruby';
|
|
349
|
+
if (/\bswift\b/.test(description))
|
|
350
|
+
return 'swift';
|
|
351
|
+
if (/\bc#\b|\bcsharp\b/.test(description))
|
|
352
|
+
return 'csharp';
|
|
353
|
+
if (/\bc\+\+|\bcpp\b/.test(description))
|
|
354
|
+
return 'cpp';
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Multi-language project heuristic: if the project's file tree contains
|
|
359
|
+
* extensions matching a non-TS language, OR the project is a Python project,
|
|
360
|
+
* treat it as a non-TS project and let the language-specific skills be relevant.
|
|
361
|
+
*/
|
|
362
|
+
function isNonTsProject(signals) {
|
|
363
|
+
if (signals.isPython)
|
|
364
|
+
return true;
|
|
365
|
+
const nonTsExts = ['swift', 'kt', 'kts', 'java', 'scala', 'py', 'pyx', 'go', 'rs', 'rb', 'cs'];
|
|
366
|
+
return nonTsExts.some((ext) => signals.hasFileExtension[ext] === true);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Discover the installed skills under `installedSkillsPath` (default:
|
|
370
|
+
* `~/.claude/skills`). Each subdir containing a SKILL.md counts as an
|
|
371
|
+
* installed skill.
|
|
372
|
+
*/
|
|
373
|
+
export async function listInstalledSkills(installedSkillsPath) {
|
|
374
|
+
if (!existsSync(installedSkillsPath))
|
|
375
|
+
return [];
|
|
376
|
+
let entries;
|
|
377
|
+
try {
|
|
378
|
+
entries = readdirSync(installedSkillsPath, { withFileTypes: true, encoding: 'utf8' });
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return [];
|
|
382
|
+
}
|
|
383
|
+
const skills = [];
|
|
384
|
+
for (const entry of entries) {
|
|
385
|
+
if (!entry.isDirectory())
|
|
386
|
+
continue;
|
|
387
|
+
const name = entry.name;
|
|
388
|
+
if (typeof name !== 'string')
|
|
389
|
+
continue;
|
|
390
|
+
const skillPath = join(installedSkillsPath, name, 'SKILL.md');
|
|
391
|
+
if (!existsSync(skillPath))
|
|
392
|
+
continue;
|
|
393
|
+
try {
|
|
394
|
+
const raw = await readFile(skillPath, 'utf8');
|
|
395
|
+
const frontmatter = parseFrontmatterLoose(raw);
|
|
396
|
+
skills.push({
|
|
397
|
+
name: frontmatter.name ?? name,
|
|
398
|
+
description: frontmatter.description ?? '',
|
|
399
|
+
skillPath,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
skills.push({ name, description: '', skillPath });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
407
|
+
return skills;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Lightweight YAML frontmatter parser (good enough for `name` + `description`).
|
|
411
|
+
* Falls back to regex when the file is malformed.
|
|
412
|
+
*/
|
|
413
|
+
function parseFrontmatterLoose(content) {
|
|
414
|
+
const lines = content.split(/\r?\n/);
|
|
415
|
+
if (lines[0] !== '---')
|
|
416
|
+
return {};
|
|
417
|
+
const end = lines.findIndex((line, index) => index > 0 && line === '---');
|
|
418
|
+
if (end === -1)
|
|
419
|
+
return {};
|
|
420
|
+
const out = {};
|
|
421
|
+
for (let i = 1; i < end; i += 1) {
|
|
422
|
+
const line = lines[i];
|
|
423
|
+
if (line === undefined)
|
|
424
|
+
continue;
|
|
425
|
+
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
426
|
+
if (match === null || match[1] === undefined)
|
|
427
|
+
continue;
|
|
428
|
+
out[match[1]] = (match[2] ?? '').replace(/^['"]|['"]$/g, '').trim();
|
|
429
|
+
}
|
|
430
|
+
return out;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* The default installed-skills path: `~/.claude/skills`. Resolved at call
|
|
434
|
+
* time so the orchestrator stays pure-ish (no module-level side effects).
|
|
435
|
+
*/
|
|
436
|
+
export function defaultInstalledSkillsPath() {
|
|
437
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
438
|
+
return join(home, '.claude', 'skills');
|
|
439
|
+
}
|
|
440
|
+
const ALWAYS_RELEVANT_SET = new Set(ALWAYS_RELEVANT_SKILLS);
|
|
441
|
+
/**
|
|
442
|
+
* Top-level orchestrator. Reads package.json + tsconfig + file tree,
|
|
443
|
+
* discovers installed skills, classifies each one, returns the JSON
|
|
444
|
+
* envelope. Idempotent: same input → same output. No filesystem writes.
|
|
445
|
+
*/
|
|
446
|
+
export async function detectSkillScope(input) {
|
|
447
|
+
const projectRoot = input.projectRoot;
|
|
448
|
+
const skillsPath = input.installedSkillsPath ?? defaultInstalledSkillsPath();
|
|
449
|
+
const signals = await extractProjectSignals(projectRoot);
|
|
450
|
+
const installed = await listInstalledSkills(skillsPath);
|
|
451
|
+
const rules = {
|
|
452
|
+
alwaysRelevant: ALWAYS_RELEVANT_SET,
|
|
453
|
+
nonTsPrefixes: NON_TS_SKILL_PREFIXES,
|
|
454
|
+
};
|
|
455
|
+
const skills = installed.map((skill) => classifySkill(skill, signals, rules));
|
|
456
|
+
const counts = {
|
|
457
|
+
relevant: skills.filter((s) => s.relevance === 'relevant').length,
|
|
458
|
+
borderline: skills.filter((s) => s.relevance === 'borderline').length,
|
|
459
|
+
irrelevant: skills.filter((s) => s.relevance === 'irrelevant').length,
|
|
460
|
+
};
|
|
461
|
+
return {
|
|
462
|
+
detectedIde: input.detectedIde ?? null,
|
|
463
|
+
projectSignals: signals,
|
|
464
|
+
skills,
|
|
465
|
+
counts,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
// ---------------------------------------------------------------------------
|
|
469
|
+
// Idempotency guard helper for tests
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
/** Compute a stable summary hash (used by tests to assert no time-dependent fields). */
|
|
472
|
+
export function detectSummary(result) {
|
|
473
|
+
const sorted = [...result.skills].sort((a, b) => a.name.localeCompare(b.name));
|
|
474
|
+
return JSON.stringify({
|
|
475
|
+
counts: result.counts,
|
|
476
|
+
skills: sorted.map((s) => ({ name: s.name, relevance: s.relevance })),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
// Quiet the "unused" warning on statSync when used only in tests paths
|
|
480
|
+
void statSync;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks skill scope` — adapter registry.
|
|
3
|
+
*
|
|
4
|
+
* The registry owns the map from IdeId → SkillScopeAdapter. It exposes two
|
|
5
|
+
* functions:
|
|
6
|
+
* - `getScopeAdapter(ide)` — direct lookup. Throws on unknown ide.
|
|
7
|
+
* - `resolveActiveAdapter(projectRoot)` — discover the best adapter by
|
|
8
|
+
* probing every registered adapter's `detect(projectRoot)`. Falls back
|
|
9
|
+
* to Claude Code with a synthetic score of 0.5 when no adapter scores
|
|
10
|
+
* ≥ 0.5 (R3: "Claude Code shipped, Trae in progress" per package.json).
|
|
11
|
+
*
|
|
12
|
+
* See tech-doc-025 §7 for the discovery flow + fallback semantics.
|
|
13
|
+
*/
|
|
14
|
+
import type { IdeId } from '../ide/ide-types.js';
|
|
15
|
+
import type { SkillScopeAdapter } from './types.js';
|
|
16
|
+
/** Get the adapter for a given IDE id. Throws on unsupported IDE. */
|
|
17
|
+
export declare function getScopeAdapter(ide: IdeId): SkillScopeAdapter;
|
|
18
|
+
/** All registered adapter ids (insertion order). */
|
|
19
|
+
export declare function listScopeAdapterIds(): readonly IdeId[];
|
|
20
|
+
/** All registered adapters (insertion order). */
|
|
21
|
+
export declare function listScopeAdapters(): readonly SkillScopeAdapter[];
|
|
22
|
+
export interface ResolvedAdapter {
|
|
23
|
+
readonly adapter: SkillScopeAdapter;
|
|
24
|
+
readonly score: number;
|
|
25
|
+
/** True when the score is synthetic (no real adapter hit ≥ 0.5). */
|
|
26
|
+
readonly isFallback: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Discover the active adapter for a project root. Returns the highest-
|
|
30
|
+
* scoring adapter; if all adapters score < 0.5, falls back to the Claude
|
|
31
|
+
* Code adapter with a synthetic score of 0.5 (R3). Stubs (Trae, Cursor,
|
|
32
|
+
* Codex, Qoder, Tongyi) return 0.0 from `detect()` so they never win.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveActiveAdapter(projectRoot: string): Promise<ResolvedAdapter>;
|
|
35
|
+
/**
|
|
36
|
+
* Test seam: replace the registry (used by stub-adapter tests to inject
|
|
37
|
+
* a fresh adapter for an IDE without restarting the module).
|
|
38
|
+
*/
|
|
39
|
+
export declare function _setScopeAdapterForTesting(ide: IdeId, adapter: SkillScopeAdapter): void;
|
|
40
|
+
/** Test seam: reset to built-in defaults. */
|
|
41
|
+
export declare function _resetScopeAdaptersForTesting(): void;
|