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.
- package/README.md +0 -53
- package/dist/src/cli/commands/core-artifact-commands.js +21 -0
- package/dist/src/cli/commands/memory-commands.d.ts +13 -0
- package/dist/src/cli/commands/memory-commands.js +60 -0
- package/dist/src/cli/commands/retrospective-commands.d.ts +9 -0
- package/dist/src/cli/commands/retrospective-commands.js +58 -0
- package/dist/src/cli/program.js +16 -22
- package/dist/src/services/fuzzy-matching/fuzzy-match-service.d.ts +15 -0
- package/dist/src/services/fuzzy-matching/fuzzy-match-service.js +56 -0
- package/dist/src/services/fuzzy-matching/types.d.ts +20 -0
- package/dist/src/services/fuzzy-matching/types.js +1 -0
- package/dist/src/services/memory/memory-search-service.d.ts +61 -0
- package/dist/src/services/memory/memory-search-service.js +80 -0
- package/dist/src/services/recommendations/capability-seed-items.js +0 -1
- package/dist/src/services/recommendations/capability-seed-mappings.js +0 -1
- package/dist/src/services/recommendations/capability-seed-sources.js +0 -1
- package/dist/src/services/retrospective/retrospective-search-service.d.ts +37 -0
- package/dist/src/services/retrospective/retrospective-search-service.js +75 -0
- package/dist/src/services/standards/project-context.d.ts +1 -1
- package/dist/src/services/standards/project-context.js +0 -4
- package/dist/src/services/standards/project-standards-service.js +1 -3
- package/dist/src/services/workspace/migrate-1-4-1-service.js +1 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +3 -7
- package/skills/peaks-solo/SKILL.md +1 -1
- package/skills/peaks-solo/references/completion-handoff.md +3 -1
- package/dist/src/cli/commands/shadcn-commands.d.ts +0 -3
- package/dist/src/cli/commands/shadcn-commands.js +0 -35
- package/dist/src/cli/commands/skill-context-stats-command.d.ts +0 -40
- package/dist/src/cli/commands/skill-context-stats-command.js +0 -96
- package/dist/src/cli/commands/skill-scope-commands.d.ts +0 -51
- package/dist/src/cli/commands/skill-scope-commands.js +0 -310
- package/dist/src/services/shadcn/shadcn-service.d.ts +0 -27
- package/dist/src/services/shadcn/shadcn-service.js +0 -128
- package/dist/src/services/skill-scope/adapters/_stub-helper.d.ts +0 -39
- package/dist/src/services/skill-scope/adapters/_stub-helper.js +0 -98
- package/dist/src/services/skill-scope/adapters/claude-code.d.ts +0 -59
- package/dist/src/services/skill-scope/adapters/claude-code.js +0 -304
- package/dist/src/services/skill-scope/adapters/codex.d.ts +0 -2
- package/dist/src/services/skill-scope/adapters/codex.js +0 -12
- package/dist/src/services/skill-scope/adapters/cursor.d.ts +0 -2
- package/dist/src/services/skill-scope/adapters/cursor.js +0 -13
- package/dist/src/services/skill-scope/adapters/qoder.d.ts +0 -2
- package/dist/src/services/skill-scope/adapters/qoder.js +0 -13
- package/dist/src/services/skill-scope/adapters/tongyi.d.ts +0 -2
- package/dist/src/services/skill-scope/adapters/tongyi.js +0 -13
- package/dist/src/services/skill-scope/adapters/trae.d.ts +0 -2
- package/dist/src/services/skill-scope/adapters/trae.js +0 -12
- package/dist/src/services/skill-scope/detect.d.ts +0 -81
- package/dist/src/services/skill-scope/detect.js +0 -513
- package/dist/src/services/skill-scope/registry.d.ts +0 -41
- package/dist/src/services/skill-scope/registry.js +0 -83
- package/dist/src/services/skill-scope/source-of-truth.d.ts +0 -44
- package/dist/src/services/skill-scope/source-of-truth.js +0 -118
- package/dist/src/services/skill-scope/types.d.ts +0 -195
- package/dist/src/services/skill-scope/types.js +0 -97
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { fuzzyMatchWithKey } from '../fuzzy-matching/fuzzy-match-service.js';
|
|
4
|
+
const DEFAULT_LIMIT = 6;
|
|
5
|
+
/**
|
|
6
|
+
* Read `.peaks/retrospective/index.json` and return the entries.
|
|
7
|
+
* Throws structured errors with stable codes that the CLI converts
|
|
8
|
+
* to the peaks envelope.
|
|
9
|
+
*/
|
|
10
|
+
function readIndex(projectRoot) {
|
|
11
|
+
const resolvedRoot = resolve(projectRoot);
|
|
12
|
+
const indexPath = join(resolvedRoot, '.peaks', 'retrospective', 'index.json');
|
|
13
|
+
if (!existsSync(indexPath)) {
|
|
14
|
+
const err = new Error(`INDEX_MISSING: retrospective 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 retrospective 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 retrospective index at ${indexPath}: ${cause.message}`);
|
|
33
|
+
err.code = 'INDEX_INVALID';
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
const index = parsed;
|
|
37
|
+
return Array.isArray(index.entries) ? index.entries : [];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Run the generic fuzzy kernel against the on-disk retrospective index.
|
|
41
|
+
* Searchable text is `title + " " + summary` per spec §Component Details.
|
|
42
|
+
* `--type` and `--outcome` filters compose with AND before the kernel
|
|
43
|
+
* runs (cheaper to filter, then fuzzy on a smaller set).
|
|
44
|
+
*/
|
|
45
|
+
export function searchRetrospective(input) {
|
|
46
|
+
if (input.query === '') {
|
|
47
|
+
const err = new Error('EMPTY_QUERY: searchRetrospective requires a non-empty query (use `peaks retrospective index` to list all)');
|
|
48
|
+
err.code = 'EMPTY_QUERY';
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
const projectRoot = input.projectRoot ?? process.cwd();
|
|
52
|
+
const limit = input.limit ?? DEFAULT_LIMIT;
|
|
53
|
+
let candidates = readIndex(projectRoot);
|
|
54
|
+
if (input.type !== undefined) {
|
|
55
|
+
candidates = candidates.filter((e) => e.type === input.type);
|
|
56
|
+
}
|
|
57
|
+
if (input.outcome !== undefined) {
|
|
58
|
+
candidates = candidates.filter((e) => e.outcome === input.outcome);
|
|
59
|
+
}
|
|
60
|
+
const matches = fuzzyMatchWithKey(input.query, candidates, { keyFn: (e) => `${e.title} ${e.summary}`, limit, caseSensitive: false });
|
|
61
|
+
return matches.map((m) => {
|
|
62
|
+
const entry = m.item;
|
|
63
|
+
return {
|
|
64
|
+
id: entry.id,
|
|
65
|
+
sessionId: entry.sessionId,
|
|
66
|
+
type: entry.type,
|
|
67
|
+
title: entry.title,
|
|
68
|
+
summary: entry.summary,
|
|
69
|
+
outcome: entry.outcome,
|
|
70
|
+
artifactPaths: entry.artifactPaths,
|
|
71
|
+
score: m.score,
|
|
72
|
+
positions: m.positions,
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type BuildTool = 'umi' | 'next' | 'vite' | 'rsbuild' | 'rspack' | 'farm' | 'craco' | 'webpack' | 'gulp' | 'angular' | 'custom' | 'unknown';
|
|
2
2
|
export type ComponentLibrary = {
|
|
3
|
-
readonly name: 'antd' | 'antd-pro' | 'mui' | '
|
|
3
|
+
readonly name: 'antd' | 'antd-pro' | 'mui' | 'element-plus' | 'element-ui' | 'arco' | 'tdesign' | 'semi' | 'nextui' | 'chakra' | 'vant' | 'none';
|
|
4
4
|
readonly majorVersion?: string;
|
|
5
5
|
readonly hasProSuite?: boolean;
|
|
6
6
|
};
|
|
@@ -64,8 +64,6 @@ function detectComponentLibrary(deps) {
|
|
|
64
64
|
}
|
|
65
65
|
if ('@mui/material' in deps)
|
|
66
66
|
return { name: 'mui', ...(majorOf(deps['@mui/material']) !== undefined ? { majorVersion: majorOf(deps['@mui/material']) } : {}) };
|
|
67
|
-
if ('tailwindcss' in deps && Object.keys(deps).some((d) => d.startsWith('@radix-ui/')))
|
|
68
|
-
return { name: 'shadcn' };
|
|
69
67
|
if ('element-plus' in deps)
|
|
70
68
|
return { name: 'element-plus' };
|
|
71
69
|
if ('element-ui' in deps)
|
|
@@ -279,8 +277,6 @@ export function componentLibraryLabel(lib) {
|
|
|
279
277
|
return 'Ant Design + Ant Design Pro';
|
|
280
278
|
case 'mui':
|
|
281
279
|
return 'Material UI';
|
|
282
|
-
case 'shadcn':
|
|
283
|
-
return 'shadcn/ui (Tailwind + Radix)';
|
|
284
280
|
case 'element-plus':
|
|
285
281
|
return 'Element Plus';
|
|
286
282
|
case 'element-ui':
|
|
@@ -159,8 +159,6 @@ function renderCommonCodingStyle(ctx) {
|
|
|
159
159
|
}
|
|
160
160
|
if (lib === 'mui')
|
|
161
161
|
stackRules.push('- Style MUI via `sx`, `styled()`, and `theme`. Do NOT apply TailwindCSS utility classes directly to MUI components.');
|
|
162
|
-
if (lib === 'shadcn')
|
|
163
|
-
stackRules.push('- Use existing shadcn component variants and Tailwind utility classes. Do not introduce a competing component library.');
|
|
164
162
|
if (ctx.cssFrameworks.includes('tailwind') && (lib === 'antd' || lib === 'antd-pro' || lib === 'mui')) {
|
|
165
163
|
stackRules.push('- TailwindCSS is for layout/utility only; component-library tokens own component styling.');
|
|
166
164
|
}
|
|
@@ -185,7 +183,7 @@ function renderCodeReview(ctx) {
|
|
|
185
183
|
const extra = [];
|
|
186
184
|
const lib = ctx.componentLibrary.name;
|
|
187
185
|
if (lib === 'antd' || lib === 'antd-pro') {
|
|
188
|
-
extra.push('- Block PRs that introduce a second component library (MUI/
|
|
186
|
+
extra.push('- Block PRs that introduce a second component library (MUI/Chakra) alongside antd.');
|
|
189
187
|
extra.push('- Block PRs that import antd v3/v4 APIs in this v5 project, or vice versa.');
|
|
190
188
|
}
|
|
191
189
|
if (ctx.cssFrameworks.includes('tailwind') && (lib === 'antd' || lib === 'antd-pro' || lib === 'mui')) {
|
|
@@ -41,7 +41,7 @@ function sha256(content) {
|
|
|
41
41
|
function enumerateLegacySessions(projectRoot) {
|
|
42
42
|
// Legacy per-session dirs: `.peaks/<sid>/` (NOT `.peaks/_runtime/<sid>/`).
|
|
43
43
|
// We skip well-known non-session entries.
|
|
44
|
-
const SKIP = new Set(['memory', 'PROJECT.md', 'retrospective', 'scope', '
|
|
44
|
+
const SKIP = new Set(['memory', 'PROJECT.md', 'retrospective', 'scope', '.peaks-init-hooks-decision.json', 'session.json', '.session.json', '_runtime', '_sub_agents', 'change', 'caller', 'callers', 'sop-state', 'system', 'active-skill.json', '.active-skill.json']);
|
|
45
45
|
const peaksRoot = join(projectRoot, '.peaks');
|
|
46
46
|
if (!existsSync(peaksRoot))
|
|
47
47
|
return [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "1.4.
|
|
1
|
+
export declare const CLI_VERSION = "1.4.2";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.4.
|
|
1
|
+
export const CLI_VERSION = "1.4.2";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-cli",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "Cross-AI-IDE workflow-gating CLI + skill family (Claude Code shipped, Trae in progress; Codex / Cursor / Qoder / Tongyi Lingma on the roadmap).",
|
|
5
5
|
"author": "SquabbyZ",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,15 +55,11 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@colbymchenry/codegraph": "0.7.10",
|
|
58
|
-
"chalk": "^5.6.2",
|
|
59
58
|
"commander": "^12.1.0",
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"shadcn": "4.7.0",
|
|
63
|
-
"terminal-kit": "^3.1.2"
|
|
59
|
+
"fzf": "^0.5.2",
|
|
60
|
+
"headroom-ai": "0.22.4"
|
|
64
61
|
},
|
|
65
62
|
"devDependencies": {
|
|
66
|
-
"@types/chalk": "^2.2.4",
|
|
67
63
|
"@types/node": "^22.10.2",
|
|
68
64
|
"@vitest/coverage-v8": "^2.1.8",
|
|
69
65
|
"tsx": "^4.19.2",
|
|
@@ -185,7 +185,7 @@ Five CLI commands harden the workflow against silent skips: `peaks request lint`
|
|
|
185
185
|
|
|
186
186
|
## Peaks-Cli Completion handoff
|
|
187
187
|
|
|
188
|
-
After final validation, refresh project-local standards via `peaks standards init/update` (never hand-write). Use Peaks-Cli TXT for the compact handoff capsule: mode, validated decisions, artifact paths, standards deltas, open questions, next action.
|
|
188
|
+
After final validation, refresh project-local standards via `peaks standards init/update` (never hand-write). Use Peaks-Cli TXT for the compact handoff capsule: mode, validated decisions, artifact paths, standards deltas, open questions, next action. **Presence management is delegated to the last downstream skill in the workflow** — peaks-solo does not call `peaks skill presence:clear` itself, and does not enforce a "no clear" rule. The downstream skills (peaks-rd, peaks-qa, peaks-txt) each manage their own presence per their respective SKILL.md.
|
|
189
189
|
|
|
190
190
|
→ see `references/completion-handoff.md` for the full handoff + "no auto-exit" rule.
|
|
191
191
|
|
|
@@ -8,7 +8,9 @@ Use Peaks-Cli TXT for the compact handoff capsule: mode, validated decisions, ar
|
|
|
8
8
|
|
|
9
9
|
## Workflow completion (no auto-exit)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
peaks-solo does NOT itself call `peaks skill presence:clear --project <repo>` at workflow end. Presence management is delegated to the last downstream skill in the workflow (peaks-rd, peaks-qa, peaks-txt); each of those skills owns its own presence:clear step per its SKILL.md. peaks-solo only sets presence: it does not unset it.
|
|
12
|
+
|
|
13
|
+
The user can continue with follow-up requirements naturally — no need to re-invoke `/peaks-solo` to do so. The header continues to display whatever skill is active; the user can `/peaks-solo` again to re-anchor.
|
|
12
14
|
|
|
13
15
|
Before ending, extract durable memories from this session:
|
|
14
16
|
```bash
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { createShadcnInvocation, executeShadcnInvocation } from '../../services/shadcn/shadcn-service.js';
|
|
2
|
-
import { fail } from '../../shared/result.js';
|
|
3
|
-
import { getErrorMessage, printResult, redactSensitiveErrorMessage } from '../cli-helpers.js';
|
|
4
|
-
function printShadcnFailure(io, error, exitCode = 1) {
|
|
5
|
-
printResult(io, fail('shadcn', 'SHADCN_COMMAND_FAILED', redactSensitiveErrorMessage(getErrorMessage(error)), {}, ['Check the shadcn command arguments before retrying']), false);
|
|
6
|
-
process.exitCode = exitCode;
|
|
7
|
-
}
|
|
8
|
-
async function runShadcnCommand(io, args) {
|
|
9
|
-
try {
|
|
10
|
-
const invocation = createShadcnInvocation({ args });
|
|
11
|
-
const result = await executeShadcnInvocation(invocation);
|
|
12
|
-
const didFail = result.exitCode !== null && result.exitCode !== 0;
|
|
13
|
-
if (result.stdout.length > 0) {
|
|
14
|
-
io.stdout((didFail ? redactSensitiveErrorMessage(result.stdout) : result.stdout).trimEnd());
|
|
15
|
-
}
|
|
16
|
-
if (result.stderr.length > 0) {
|
|
17
|
-
io.stderr((didFail ? redactSensitiveErrorMessage(result.stderr) : result.stderr).trimEnd());
|
|
18
|
-
}
|
|
19
|
-
if (didFail) {
|
|
20
|
-
process.exitCode = result.exitCode;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
catch (error) {
|
|
24
|
-
printShadcnFailure(io, error);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
export function registerShadcnCommands(program, io) {
|
|
28
|
-
program
|
|
29
|
-
.command('shadcn')
|
|
30
|
-
.description('Run the pinned shadcn CLI bundled with Peaks')
|
|
31
|
-
.allowUnknownOption(true)
|
|
32
|
-
.helpOption(false)
|
|
33
|
-
.argument('<args...>', 'arguments forwarded to shadcn')
|
|
34
|
-
.action((args) => runShadcnCommand(io, args));
|
|
35
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `peaks skill context-stats` — R003.2
|
|
3
|
-
*
|
|
4
|
-
* Reports the per-project skill context footprint for the LLM:
|
|
5
|
-
* - Total bytes of allowed skills (full SKILL.md)
|
|
6
|
-
* - Total bytes of denied skills (shadow stubs in the project-local mirror)
|
|
7
|
-
* - Estimated token counts (chars/4 for full skills, bytes*0.25 for stubs)
|
|
8
|
-
* - Shadow-stub reduction percentage vs. the original full SKILL.md bytes
|
|
9
|
-
*
|
|
10
|
-
* If no scope is applied (no `.peaks/scope/skills.json`), returns the
|
|
11
|
-
* "no-scope" branch with a recommended command.
|
|
12
|
-
*/
|
|
13
|
-
import { type ResultEnvelope } from '../../shared/result.js';
|
|
14
|
-
export interface RunContextStatsInput {
|
|
15
|
-
readonly projectRoot: string;
|
|
16
|
-
readonly json: boolean;
|
|
17
|
-
/** Average bytes-per-skill estimate for denied skills without a real SKILL.md (default 7000). */
|
|
18
|
-
readonly estimatedDeniedOriginalBytes?: number;
|
|
19
|
-
}
|
|
20
|
-
export interface ContextStatsData {
|
|
21
|
-
readonly scope: unknown;
|
|
22
|
-
readonly totals: {
|
|
23
|
-
readonly allowedCount: number;
|
|
24
|
-
readonly deniedCount: number;
|
|
25
|
-
readonly allowedBytes: number;
|
|
26
|
-
readonly stubBytes: number;
|
|
27
|
-
readonly originalDeniedBytes: number;
|
|
28
|
-
readonly shadowReductionPct: number;
|
|
29
|
-
readonly totalBytes: number;
|
|
30
|
-
};
|
|
31
|
-
readonly estimatedTokens: {
|
|
32
|
-
readonly allowed: number;
|
|
33
|
-
readonly denied: number;
|
|
34
|
-
readonly total: number;
|
|
35
|
-
};
|
|
36
|
-
readonly message?: string;
|
|
37
|
-
readonly recommendedCommand?: string;
|
|
38
|
-
readonly human?: string;
|
|
39
|
-
}
|
|
40
|
-
export declare function runContextStats(input: RunContextStatsInput): Promise<ResultEnvelope<ContextStatsData>>;
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `peaks skill context-stats` — R003.2
|
|
3
|
-
*
|
|
4
|
-
* Reports the per-project skill context footprint for the LLM:
|
|
5
|
-
* - Total bytes of allowed skills (full SKILL.md)
|
|
6
|
-
* - Total bytes of denied skills (shadow stubs in the project-local mirror)
|
|
7
|
-
* - Estimated token counts (chars/4 for full skills, bytes*0.25 for stubs)
|
|
8
|
-
* - Shadow-stub reduction percentage vs. the original full SKILL.md bytes
|
|
9
|
-
*
|
|
10
|
-
* If no scope is applied (no `.peaks/scope/skills.json`), returns the
|
|
11
|
-
* "no-scope" branch with a recommended command.
|
|
12
|
-
*/
|
|
13
|
-
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
14
|
-
import { join } from 'node:path';
|
|
15
|
-
import { ok, fail } from '../../shared/result.js';
|
|
16
|
-
export async function runContextStats(input) {
|
|
17
|
-
const projectRoot = input.projectRoot;
|
|
18
|
-
const scopePath = join(projectRoot, '.peaks', 'scope', 'skills.json');
|
|
19
|
-
if (!existsSync(scopePath)) {
|
|
20
|
-
const noScopeData = {
|
|
21
|
-
scope: null,
|
|
22
|
-
totals: {
|
|
23
|
-
allowedCount: 0,
|
|
24
|
-
deniedCount: 0,
|
|
25
|
-
allowedBytes: 0,
|
|
26
|
-
stubBytes: 0,
|
|
27
|
-
originalDeniedBytes: 0,
|
|
28
|
-
shadowReductionPct: 0,
|
|
29
|
-
totalBytes: 0,
|
|
30
|
-
},
|
|
31
|
-
estimatedTokens: { allowed: 0, denied: 0, total: 0 },
|
|
32
|
-
message: 'No scope applied. Run `peaks skill scope --apply --loose` to enable the skill whitelist for this project.',
|
|
33
|
-
recommendedCommand: 'peaks skill scope --apply --loose',
|
|
34
|
-
};
|
|
35
|
-
return fail('skill.context-stats', 'NO_SCOPE', 'No scope applied to this project.', noScopeData);
|
|
36
|
-
}
|
|
37
|
-
const raw = JSON.parse(readFileSync(scopePath, 'utf8'));
|
|
38
|
-
const allowlist = raw.allowlist;
|
|
39
|
-
const denied = [];
|
|
40
|
-
// Walk .claude/skills/ to find shadow stubs (denied skills not in allowlist).
|
|
41
|
-
const skillsMirror = join(projectRoot, '.claude', 'skills');
|
|
42
|
-
let stubBytes = 0;
|
|
43
|
-
let originalDeniedBytes = 0;
|
|
44
|
-
const estimatedDeniedOriginalBytes = input.estimatedDeniedOriginalBytes ?? 7000;
|
|
45
|
-
if (existsSync(skillsMirror)) {
|
|
46
|
-
for (const entry of readdirSync(skillsMirror)) {
|
|
47
|
-
const skillMd = join(skillsMirror, entry, 'SKILL.md');
|
|
48
|
-
if (!existsSync(skillMd))
|
|
49
|
-
continue;
|
|
50
|
-
if (allowlist.includes(entry))
|
|
51
|
-
continue; // allowed skills are NOT in the mirror
|
|
52
|
-
denied.push(entry);
|
|
53
|
-
const stat = statSync(skillMd);
|
|
54
|
-
stubBytes += stat.size;
|
|
55
|
-
// Heuristic: a denied skill would have loaded its full body (~7000 bytes) without shadow-fallback.
|
|
56
|
-
// We use this as the "original" to compute the reduction.
|
|
57
|
-
originalDeniedBytes += estimatedDeniedOriginalBytes;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
// For allowed skills, compute the sum of their original SKILL.md bytes from the global catalog.
|
|
61
|
-
// We don't know the global catalog path here; estimate at 364 KB / 44 = 8272 bytes per allowed skill.
|
|
62
|
-
const allowedBytes = allowlist.length * 8272; // matches the R002 measurement
|
|
63
|
-
const totalBytes = allowedBytes + stubBytes;
|
|
64
|
-
const shadowReductionPct = originalDeniedBytes > 0 ? 1 - stubBytes / originalDeniedBytes : 0;
|
|
65
|
-
// Token estimation: chars / 4 for full skills; bytes * 0.25 for stubs (YAML is denser).
|
|
66
|
-
const allowedTokens = Math.round(allowedBytes / 4);
|
|
67
|
-
const deniedTokens = Math.round(stubBytes * 0.25);
|
|
68
|
-
const totalTokens = allowedTokens + deniedTokens;
|
|
69
|
-
const totals = {
|
|
70
|
-
allowedCount: allowlist.length,
|
|
71
|
-
deniedCount: denied.length,
|
|
72
|
-
allowedBytes,
|
|
73
|
-
stubBytes,
|
|
74
|
-
originalDeniedBytes,
|
|
75
|
-
shadowReductionPct,
|
|
76
|
-
totalBytes,
|
|
77
|
-
};
|
|
78
|
-
const estimatedTokens = {
|
|
79
|
-
allowed: allowedTokens,
|
|
80
|
-
denied: deniedTokens,
|
|
81
|
-
total: totalTokens,
|
|
82
|
-
};
|
|
83
|
-
const data = {
|
|
84
|
-
scope: raw,
|
|
85
|
-
totals,
|
|
86
|
-
estimatedTokens,
|
|
87
|
-
};
|
|
88
|
-
if (!input.json) {
|
|
89
|
-
data.human = [
|
|
90
|
-
`Allowed: ${allowlist.length} skills, ${(allowedBytes / 1024).toFixed(1)} KB / ${(allowedTokens / 1000).toFixed(1)}K tokens.`,
|
|
91
|
-
`Denied: ${denied.length} skills, ${(stubBytes / 1024).toFixed(1)} KB / ${(deniedTokens / 1000).toFixed(1)}K tokens (${(shadowReductionPct * 100).toFixed(1)}% shadow-stub reduction).`,
|
|
92
|
-
`Total: ${(totalBytes / 1024).toFixed(1)} KB / ${(totalTokens / 1000).toFixed(1)}K tokens.`,
|
|
93
|
-
].join('\n');
|
|
94
|
-
}
|
|
95
|
-
return ok('skill.context-stats', data);
|
|
96
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `peaks skill scope` CLI surface (slice 025.1).
|
|
3
|
-
*
|
|
4
|
-
* Four subcommands (mutually exclusive):
|
|
5
|
-
* - `--detect` — dry-run; prints the relevance matrix, never touches files.
|
|
6
|
-
* - `--apply` — writes the source-of-truth + IDE-native config.
|
|
7
|
-
* - `--show` — reads the source-of-truth + native config back.
|
|
8
|
-
* - `--reset` — removes the source-of-truth + IDE-native config.
|
|
9
|
-
*
|
|
10
|
-
* Exit code matrix (tech-doc §6.3):
|
|
11
|
-
* 0 success
|
|
12
|
-
* 1 uncaught error
|
|
13
|
-
* 2 invalid usage (missing/incompatible flags)
|
|
14
|
-
* 3 source-of-truth written but adapter returned NOT_SUPPORTED
|
|
15
|
-
* 4 adapter failure other than NOT_SUPPORTED
|
|
16
|
-
*/
|
|
17
|
-
import { Command } from 'commander';
|
|
18
|
-
import { type ResultEnvelope } from '../../shared/result.js';
|
|
19
|
-
import { type ProgramIO } from '../cli-helpers.js';
|
|
20
|
-
export type SkillScopeAction = 'detect' | 'apply' | 'show' | 'reset';
|
|
21
|
-
export interface RunSkillScopeInput {
|
|
22
|
-
readonly subcommand: SkillScopeAction;
|
|
23
|
-
readonly project: string;
|
|
24
|
-
readonly strict?: boolean;
|
|
25
|
-
readonly loose?: boolean;
|
|
26
|
-
readonly ide?: string;
|
|
27
|
-
readonly shadowFallback?: boolean;
|
|
28
|
-
readonly json?: boolean;
|
|
29
|
-
/** Test seam: override the detected allowlist (CLI re-adds peaks-* per G6). */
|
|
30
|
-
readonly overrideAllowlist?: readonly string[];
|
|
31
|
-
/** Test seam: force the source-of-truth write to fail (simulates atomicity test). */
|
|
32
|
-
readonly simulateSourceOfTruthWriteFailure?: boolean;
|
|
33
|
-
}
|
|
34
|
-
export interface RunSkillScopeResult {
|
|
35
|
-
readonly exitCode: number;
|
|
36
|
-
readonly envelope: ResultEnvelope<unknown> | null;
|
|
37
|
-
readonly stdout: string;
|
|
38
|
-
readonly stderr: string;
|
|
39
|
-
}
|
|
40
|
-
/** Run the --apply subcommand. R003.3: `runApply` is exported for direct testing. */
|
|
41
|
-
export declare function runApply(input: RunSkillScopeInput): Promise<RunSkillScopeResult>;
|
|
42
|
-
/**
|
|
43
|
-
* Programmatic entry point for `peaks skill scope`. Used by the CLI shim
|
|
44
|
-
* AND by the unit tests.
|
|
45
|
-
*/
|
|
46
|
-
export declare function runSkillScopeCommand(input: RunSkillScopeInput): Promise<RunSkillScopeResult>;
|
|
47
|
-
/**
|
|
48
|
-
* Register the `peaks skill scope` subcommand on the `skill` command group.
|
|
49
|
-
* Mutually-exclusive flags: exactly one of --detect / --apply / --show / --reset.
|
|
50
|
-
*/
|
|
51
|
-
export declare function registerSkillScopeCommands(program: Command, io: ProgramIO): void;
|