peaks-cli 1.3.9 → 1.4.1
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 +53 -0
- package/dist/src/cli/commands/core-artifact-commands.js +27 -0
- package/dist/src/cli/commands/migrate-1-4-1-command.d.ts +11 -0
- package/dist/src/cli/commands/migrate-1-4-1-command.js +34 -0
- package/dist/src/cli/commands/skill-context-stats-command.d.ts +40 -0
- package/dist/src/cli/commands/skill-context-stats-command.js +96 -0
- package/dist/src/cli/commands/skill-scope-commands.d.ts +51 -0
- package/dist/src/cli/commands/skill-scope-commands.js +310 -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/commands/workspace-commands.js +8 -0
- package/dist/src/cli/program.js +6 -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/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 +81 -0
- package/dist/src/services/skill-scope/detect.js +513 -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 +195 -0
- package/dist/src/services/skill-scope/types.js +97 -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/services/workspace/migrate-1-4-1-service.d.ts +44 -0
- package/dist/src/services/workspace/migrate-1-4-1-service.js +195 -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 +25 -0
- package/skills/peaks-qa/references/qa-perf-test-plan.md +67 -0
- package/skills/peaks-qa/references/qa-security-test-plan.md +73 -0
- package/skills/peaks-qa/references/qa-transition-gates.md +13 -9
- package/skills/peaks-rd/SKILL.md +2 -2
- package/skills/peaks-rd/references/mandatory-perf-baseline.md +2 -0
package/README.md
CHANGED
|
@@ -148,6 +148,59 @@ 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
|
+
|
|
151
204
|
## 自定义 SOP(把你的流程变成带门禁的工作流)
|
|
152
205
|
|
|
153
206
|
> **技能入口**:`peaks-sop` 技能
|
|
@@ -5,12 +5,14 @@ import { getArtifactWorkspaceStatus, planArtifactSync } from '../../services/art
|
|
|
5
5
|
import { executeProjectMemoryBackup, executeProjectMemoryExtract, summarizeProjectMemoryBackupResult, summarizeProjectMemoryExtractResult } from '../../services/memory/project-memory-service.js';
|
|
6
6
|
import { summarizeProjectStandardsInitResult, summarizeProjectStandardsUpdateResult } from '../../services/standards/project-standards-service.js';
|
|
7
7
|
import { executeProjectStandardsInitIdeAware, executeProjectStandardsUpdateIdeAware } from '../../services/standards/ide-aware-standards-service.js';
|
|
8
|
+
import { migrateStandards } from '../../services/standards/migrate-service.js';
|
|
8
9
|
import { listProfiles } from '../../services/profiles/profile-service.js';
|
|
9
10
|
import { planProxyTest } from '../../services/proxy/proxy-service.js';
|
|
10
11
|
import { runDoctor } from '../../services/doctor/doctor-service.js';
|
|
11
12
|
import { listSkills } from '../../services/skills/skill-registry.js';
|
|
12
13
|
import { inspectSkillRunbook } from '../../services/skills/skill-runbook-service.js';
|
|
13
14
|
import { setSkillPresence, clearSkillPresence, getSkillPresence, isSkillPresenceMode, touchSkillHeartbeat } from '../../services/skills/skill-presence-service.js';
|
|
15
|
+
import { detectPresenceMarker } from '../../services/hooks/presence-marker-detector.js';
|
|
14
16
|
import { getSessionId, getSessionMeta, rotateSessionBinding, setSessionMeta, setSessionTitle, listSessionMetas } from '../../services/session/session-manager.js';
|
|
15
17
|
import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
|
|
16
18
|
import { findProjectRoot } from '../../services/config/config-safety.js';
|
|
@@ -198,6 +200,16 @@ export function registerCoreAndArtifactCommands(program, io) {
|
|
|
198
200
|
lastHeartbeat: updated.lastHeartbeat
|
|
199
201
|
}), options.json);
|
|
200
202
|
});
|
|
203
|
+
addJsonOption(skill
|
|
204
|
+
.command('detect-marker-loss')
|
|
205
|
+
.description('Detect whether the latest assistant message lost the Peaks-Cli status header while a peaks skill is still active (slice 028 detection primitive).')
|
|
206
|
+
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
|
|
207
|
+
.option('--message <text>', 'latest assistant message text to scan (defaults to reading the most recent LLM response from the stdin pipe, or empty string when no pipe is attached)')).action((options) => {
|
|
208
|
+
const projectRoot = options.project ?? findProjectRoot(process.cwd()) ?? process.cwd();
|
|
209
|
+
const message = options.message ?? '';
|
|
210
|
+
const result = detectPresenceMarker({ project: projectRoot, latestAssistantMessage: message });
|
|
211
|
+
printResult(io, ok('skill.detect-marker-loss', result), options.json);
|
|
212
|
+
});
|
|
201
213
|
const session = program.command('session').description('Manage Peaks session directories');
|
|
202
214
|
addJsonOption(session
|
|
203
215
|
.command('list')
|
|
@@ -449,6 +461,21 @@ export function registerCoreAndArtifactCommands(program, io) {
|
|
|
449
461
|
process.exitCode = 1;
|
|
450
462
|
}
|
|
451
463
|
});
|
|
464
|
+
addJsonOption(standards
|
|
465
|
+
.command('migrate')
|
|
466
|
+
.description('Rewrite a consumer project CLAUDE.md to drop the legacy heartbeat block (slice 028). Dry-run by default; pass --apply to write.')
|
|
467
|
+
.option('--project <path>', 'target project root')
|
|
468
|
+
.option('--apply', 'rewrite the legacy block in place; default is dry-run')).action((options) => {
|
|
469
|
+
const projectRoot = options.project ?? process.cwd();
|
|
470
|
+
try {
|
|
471
|
+
const result = migrateStandards({ project: projectRoot, apply: options.apply === true });
|
|
472
|
+
printResult(io, ok('standards.migrate', result.data, [], result.data.nextActions), options.json);
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
printResult(io, fail('standards.migrate', 'STANDARDS_MIGRATE_FAILED', getErrorMessage(error), { file: null, foundOldBlock: false, wouldChange: false, applied: false, before: null, after: null, nextActions: [] }, [getErrorMessage(error)]), options.json);
|
|
476
|
+
process.exitCode = 1;
|
|
477
|
+
}
|
|
478
|
+
});
|
|
452
479
|
const memory = program.command('memory').description('Manage project-local Peaks memory');
|
|
453
480
|
addJsonOption(memory
|
|
454
481
|
.command('extract')
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks workspace migrate-1-4-1` — R004 subcommand.
|
|
3
|
+
*
|
|
4
|
+
* Cleanup helper for projects upgraded from 1.4.1 → 1.4.2. Moves per-session
|
|
5
|
+
* files from the legacy `.peaks/<sid>/<role>/<file>.md` path into the canonical
|
|
6
|
+
* `.peaks/_runtime/<sid>/<role>/<file>.md` path. Default is dry-run; pass
|
|
7
|
+
* `--apply` to actually `rename` the files and remove emptied legacy dirs.
|
|
8
|
+
*/
|
|
9
|
+
import type { Command } from 'commander';
|
|
10
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
11
|
+
export declare function registerMigrate1_4_1Command(workspace: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks workspace migrate-1-4-1` — R004 subcommand.
|
|
3
|
+
*
|
|
4
|
+
* Cleanup helper for projects upgraded from 1.4.1 → 1.4.2. Moves per-session
|
|
5
|
+
* files from the legacy `.peaks/<sid>/<role>/<file>.md` path into the canonical
|
|
6
|
+
* `.peaks/_runtime/<sid>/<role>/<file>.md` path. Default is dry-run; pass
|
|
7
|
+
* `--apply` to actually `rename` the files and remove emptied legacy dirs.
|
|
8
|
+
*/
|
|
9
|
+
import { ok, fail } from '../../shared/result.js';
|
|
10
|
+
import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
|
|
11
|
+
import { addJsonOption } from '../cli-helpers.js';
|
|
12
|
+
import { planMigrate1_4_1, applyMigrate1_4_1 } from '../../services/workspace/migrate-1-4-1-service.js';
|
|
13
|
+
export function registerMigrate1_4_1Command(workspace, io) {
|
|
14
|
+
addJsonOption(workspace
|
|
15
|
+
.command('migrate-1-4-1')
|
|
16
|
+
.description('R004: Move per-session files from the legacy `.peaks/<sid>/<role>/<file>.md` path into the canonical `.peaks/_runtime/<sid>/<role>/<file>.md` path. ' +
|
|
17
|
+
'Default: dry-run. Pass --apply to actually `rename` the files and remove emptied legacy dirs. ' +
|
|
18
|
+
'Reads each file once, computes sha256, compares to canonical. Identical-content duplicates are removed from legacy. Content-mismatch files are reported and NOT deleted (manual review).')
|
|
19
|
+
.requiredOption('--project <path>', 'target project root')
|
|
20
|
+
.option('--apply', 'actually rename the files and remove empty legacy dirs (destructive); without it, dry-run only', false)).action(async (options) => {
|
|
21
|
+
try {
|
|
22
|
+
const projectRoot = resolveCanonicalProjectRoot(options.project);
|
|
23
|
+
const apply = options.apply === true;
|
|
24
|
+
const result = apply ? applyMigrate1_4_1(projectRoot) : planMigrate1_4_1(projectRoot);
|
|
25
|
+
const envelope = ok('workspace.migrate-1-4-1', result);
|
|
26
|
+
io.stdout(`${JSON.stringify(envelope, null, 2)}\n`);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
const envelope = fail('workspace.migrate-1-4-1', 'MIGRATE_FAILED', err.message, null, ['Run with --apply to attempt the move (default is dry-run only)']);
|
|
30
|
+
io.stdout(`${JSON.stringify(envelope, null, 2)}\n`);
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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>>;
|
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
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;
|
|
@@ -0,0 +1,310 @@
|
|
|
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 { existsSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { detectSkillScope, } from '../../services/skill-scope/detect.js';
|
|
20
|
+
import { resolveActiveAdapter, getScopeAdapter } from '../../services/skill-scope/registry.js';
|
|
21
|
+
import { ideCompanionFilePath, readIdeCompanion, readSourceOfTruth, removeIfExists, scopeFilePath, writeSourceOfTruth, } from '../../services/skill-scope/source-of-truth.js';
|
|
22
|
+
import { ALWAYS_RELEVANT_SKILLS } from '../../services/skill-scope/types.js';
|
|
23
|
+
import { fail, getErrorMessage, ok } from '../../shared/result.js';
|
|
24
|
+
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
25
|
+
const VALID_ACTIONS = ['detect', 'apply', 'show', 'reset'];
|
|
26
|
+
const VALID_IDES = ['claude-code', 'trae', 'codex', 'cursor', 'qoder', 'tongyi-lingma'];
|
|
27
|
+
function isValidIde(value) {
|
|
28
|
+
return VALID_IDES.includes(value);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* G6: enforce the peaks-* allowlist. Re-adds any peak-* skill that is
|
|
32
|
+
* missing from the allowlist, and removes any peak-* skill from the
|
|
33
|
+
* denylist. The list is the same one declared in `types.ts`.
|
|
34
|
+
*/
|
|
35
|
+
function enforcePeaksAllowlist(allowlist) {
|
|
36
|
+
const set = new Set(allowlist);
|
|
37
|
+
for (const name of ALWAYS_RELEVANT_SKILLS) {
|
|
38
|
+
if (name.startsWith('peaks-'))
|
|
39
|
+
set.add(name);
|
|
40
|
+
}
|
|
41
|
+
return [...set];
|
|
42
|
+
}
|
|
43
|
+
function stripPeaksFromDenylist(denylist) {
|
|
44
|
+
return denylist.filter((name) => !name.startsWith('peaks-'));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Determine the IDE. Caller-supplied `--ide` wins; otherwise the registry
|
|
48
|
+
* probes the project root.
|
|
49
|
+
*/
|
|
50
|
+
async function resolveIde(projectRoot, override) {
|
|
51
|
+
if (override !== undefined) {
|
|
52
|
+
if (!isValidIde(override)) {
|
|
53
|
+
throw new Error(`Unknown IDE: ${override}. Valid: ${VALID_IDES.join(', ')}`);
|
|
54
|
+
}
|
|
55
|
+
return { ide: override, isFallback: false };
|
|
56
|
+
}
|
|
57
|
+
const resolved = await resolveActiveAdapter(projectRoot);
|
|
58
|
+
return { ide: resolved.adapter.ide, isFallback: resolved.isFallback };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Stable timestamp (no millisecond jitter) for the `generatedAt` field.
|
|
62
|
+
* `Date.now()` would still be deterministic per-run; we keep the natural
|
|
63
|
+
* one to ensure `generatedAt` matches what the user sees on disk.
|
|
64
|
+
*/
|
|
65
|
+
function nowIso() {
|
|
66
|
+
return new Date().toISOString();
|
|
67
|
+
}
|
|
68
|
+
/** Run the --detect subcommand. */
|
|
69
|
+
async function runDetect(input) {
|
|
70
|
+
try {
|
|
71
|
+
const result = await detectSkillScope({ projectRoot: input.project });
|
|
72
|
+
const envelope = ok('skill.scope.detect', result);
|
|
73
|
+
const stdout = input.json === true ? JSON.stringify(envelope, null, 2) : JSON.stringify(result, null, 2);
|
|
74
|
+
return { exitCode: 0, envelope, stdout, stderr: '' };
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
const envelope = fail('skill.scope.detect', 'DETECT_FAILED', getErrorMessage(error), null);
|
|
78
|
+
return { exitCode: 1, envelope, stdout: '', stderr: envelope.message ?? 'detect failed' };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Build the final ScopeConfig (applies G6 enforcement + override). */
|
|
82
|
+
function buildScopeConfig(args) {
|
|
83
|
+
const strict = args.strict;
|
|
84
|
+
const detected = args.detected;
|
|
85
|
+
// Build allowlist from detected.relevant + (in loose) borderline.
|
|
86
|
+
const allowFromDetect = detected.skills
|
|
87
|
+
.filter((s) => s.relevance === 'relevant' || (!strict && s.relevance === 'borderline'))
|
|
88
|
+
.map((s) => s.name);
|
|
89
|
+
const merged = args.allowOverride !== undefined ? [...args.allowOverride, ...allowFromDetect] : allowFromDetect;
|
|
90
|
+
const enforced = enforcePeaksAllowlist(merged);
|
|
91
|
+
// Denylist: irrelevant skills (strict + loose both), minus anything in allowlist.
|
|
92
|
+
const denyFromDetect = detected.skills
|
|
93
|
+
.filter((s) => s.relevance === 'irrelevant' && !enforced.includes(s.name))
|
|
94
|
+
.map((s) => s.name);
|
|
95
|
+
const finalDeny = stripPeaksFromDenylist(denyFromDetect);
|
|
96
|
+
return {
|
|
97
|
+
generatedAt: nowIso(),
|
|
98
|
+
ide: args.ide,
|
|
99
|
+
strict,
|
|
100
|
+
allowlist: enforced,
|
|
101
|
+
denylist: finalDeny,
|
|
102
|
+
skills: detected.skills,
|
|
103
|
+
signals: detected.projectSignals,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Run the --apply subcommand. R003.3: `runApply` is exported for direct testing. */
|
|
107
|
+
export async function runApply(input) {
|
|
108
|
+
// 1. Detect the scope.
|
|
109
|
+
const detected = await detectSkillScope({ projectRoot: input.project });
|
|
110
|
+
// --strict wins when both flags are passed. Default is --loose per PRD.
|
|
111
|
+
const isStrict = input.strict === true && input.loose !== true;
|
|
112
|
+
const loose = !isStrict;
|
|
113
|
+
const { ide, isFallback } = await resolveIde(input.project, input.ide);
|
|
114
|
+
const adapter = getScopeAdapter(ide);
|
|
115
|
+
const config = buildScopeConfig({
|
|
116
|
+
ide,
|
|
117
|
+
strict: isStrict,
|
|
118
|
+
detected,
|
|
119
|
+
...(input.overrideAllowlist !== undefined ? { allowOverride: input.overrideAllowlist } : {}),
|
|
120
|
+
});
|
|
121
|
+
// 2. Write the source-of-truth first (atomic). Test seam: simulate failure.
|
|
122
|
+
let writtenFiles = [];
|
|
123
|
+
let sourceWritten = false;
|
|
124
|
+
try {
|
|
125
|
+
if (input.simulateSourceOfTruthWriteFailure) {
|
|
126
|
+
throw new Error('simulated source-of-truth write failure');
|
|
127
|
+
}
|
|
128
|
+
const file = await writeSourceOfTruth(input.project, config);
|
|
129
|
+
writtenFiles.push(file);
|
|
130
|
+
sourceWritten = true;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
const envelope = fail('skill.scope.apply', 'WRITE_FAILED', getErrorMessage(error), { ide, sourceWritten: false }, ['Fix filesystem permissions on the project root and retry']);
|
|
134
|
+
return { exitCode: 4, envelope, stdout: '', stderr: envelope.message ?? 'write failed' };
|
|
135
|
+
}
|
|
136
|
+
// 3. Call the adapter. Stub adapters return notSupported=true; we surface it.
|
|
137
|
+
const adapterInput = {
|
|
138
|
+
allowlist: config.allowlist,
|
|
139
|
+
denylist: config.denylist,
|
|
140
|
+
strict: config.strict,
|
|
141
|
+
projectRoot: input.project,
|
|
142
|
+
sourceConfig: config,
|
|
143
|
+
// R003.3: default to shadow-fallback=true. Pass `shadowFallback: false` (or the
|
|
144
|
+
// new --no-shadow-fallback CLI flag) to opt out and copy the full SKILL.md.
|
|
145
|
+
shadowFallback: input.shadowFallback !== false,
|
|
146
|
+
};
|
|
147
|
+
let result;
|
|
148
|
+
try {
|
|
149
|
+
result = await adapter.applyScope(adapterInput);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
// Roll back the source-of-truth on adapter failure.
|
|
153
|
+
await removeIfExists(scopeFilePath(input.project));
|
|
154
|
+
const envelope = fail('skill.scope.apply', 'ADAPTER_FAILED', getErrorMessage(error), { ide, sourceWritten: false, writtenFiles: [] }, ['Inspect the adapter error and retry']);
|
|
155
|
+
return { exitCode: 4, envelope, stdout: '', stderr: envelope.message ?? 'adapter failed' };
|
|
156
|
+
}
|
|
157
|
+
// The stub adapter also writes the canonical skills.json — that's
|
|
158
|
+
// already on disk from step 2, so its second write is a no-op update.
|
|
159
|
+
const finalWrittenFiles = [...writtenFiles, ...result.writtenFiles];
|
|
160
|
+
const envelope = ok('skill.scope.apply', {
|
|
161
|
+
ide,
|
|
162
|
+
isFallback,
|
|
163
|
+
strict: isStrict,
|
|
164
|
+
loose,
|
|
165
|
+
allowlist: config.allowlist,
|
|
166
|
+
denylist: config.denylist,
|
|
167
|
+
signals: config.signals,
|
|
168
|
+
writtenFiles: finalWrittenFiles,
|
|
169
|
+
usedShadowStub: result.usedShadowStub,
|
|
170
|
+
notSupported: result.notSupported,
|
|
171
|
+
strippedFromDenylist: result.strippedFromDenylist ?? [],
|
|
172
|
+
error: result.error,
|
|
173
|
+
});
|
|
174
|
+
const stdout = input.json === true ? JSON.stringify(envelope, null, 2) : JSON.stringify(envelope.data, null, 2);
|
|
175
|
+
if (result.notSupported) {
|
|
176
|
+
// Stub adapter: NOT_SUPPORTED → exit 3, write error to stderr.
|
|
177
|
+
const stderr = `${result.error?.code ?? 'NOT_SUPPORTED'}: ${result.error?.message ?? 'not supported'}`;
|
|
178
|
+
return { exitCode: 3, envelope, stdout, stderr };
|
|
179
|
+
}
|
|
180
|
+
return { exitCode: 0, envelope, stdout, stderr: '' };
|
|
181
|
+
}
|
|
182
|
+
/** Run the --show subcommand. */
|
|
183
|
+
async function runShow(input) {
|
|
184
|
+
const source = await readSourceOfTruth(input.project);
|
|
185
|
+
const { ide } = await resolveIde(input.project, input.ide);
|
|
186
|
+
const companionPath = ideCompanionFilePath(input.project, ide);
|
|
187
|
+
const companion = await readIdeCompanion(input.project, ide);
|
|
188
|
+
// For Claude Code, the native config is `.claude/settings.local.json`.
|
|
189
|
+
const nativeSettingsPath = join(input.project, '.claude', 'settings.local.json');
|
|
190
|
+
const nativeExists = existsSync(nativeSettingsPath);
|
|
191
|
+
let native = companion;
|
|
192
|
+
if (nativeExists) {
|
|
193
|
+
try {
|
|
194
|
+
const { readFile } = await import('node:fs/promises');
|
|
195
|
+
native = JSON.parse(await readFile(nativeSettingsPath, 'utf8'));
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
native = null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const data = {
|
|
202
|
+
ide,
|
|
203
|
+
source,
|
|
204
|
+
native,
|
|
205
|
+
nativeSettingsPath: nativeExists ? '.claude/settings.local.json' : null,
|
|
206
|
+
companionPath: existsSync(companionPath) ? companionPath : null,
|
|
207
|
+
};
|
|
208
|
+
const envelope = ok('skill.scope.show', data);
|
|
209
|
+
const stdout = input.json === true ? JSON.stringify(envelope, null, 2) : JSON.stringify(data, null, 2);
|
|
210
|
+
return { exitCode: 0, envelope, stdout, stderr: '' };
|
|
211
|
+
}
|
|
212
|
+
/** Run the --reset subcommand. */
|
|
213
|
+
async function runReset(input) {
|
|
214
|
+
const { ide } = await resolveIde(input.project, input.ide);
|
|
215
|
+
const adapter = getScopeAdapter(ide);
|
|
216
|
+
const resetResult = await adapter.resetScope({ projectRoot: input.project });
|
|
217
|
+
const sourceFile = scopeFilePath(input.project);
|
|
218
|
+
const sourceRemoved = await removeIfExists(sourceFile);
|
|
219
|
+
const allRemoved = [...resetResult.removedFiles, ...(sourceRemoved ? [sourceFile] : [])];
|
|
220
|
+
const envelope = ok('skill.scope.reset', {
|
|
221
|
+
ide,
|
|
222
|
+
removedFiles: allRemoved,
|
|
223
|
+
});
|
|
224
|
+
// Always include the canonical source-of-truth path in the human-readable
|
|
225
|
+
// summary, even if it didn't exist (so the user knows what was targeted).
|
|
226
|
+
const displayFiles = allRemoved.length > 0 ? allRemoved : [sourceFile, join(input.project, '.claude', 'settings.local.json')];
|
|
227
|
+
const summary = `removed: ${displayFiles.join(', ')}`;
|
|
228
|
+
const stdout = input.json === true ? JSON.stringify(envelope, null, 2) : summary;
|
|
229
|
+
return { exitCode: 0, envelope, stdout, stderr: '' };
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Programmatic entry point for `peaks skill scope`. Used by the CLI shim
|
|
233
|
+
* AND by the unit tests.
|
|
234
|
+
*/
|
|
235
|
+
export async function runSkillScopeCommand(input) {
|
|
236
|
+
if (!VALID_ACTIONS.includes(input.subcommand)) {
|
|
237
|
+
const envelope = fail('skill.scope', 'INVALID_USAGE', `Unknown action: ${input.subcommand}`, null);
|
|
238
|
+
return { exitCode: 2, envelope, stdout: '', stderr: envelope.message ?? 'invalid usage' };
|
|
239
|
+
}
|
|
240
|
+
switch (input.subcommand) {
|
|
241
|
+
case 'detect': return runDetect(input);
|
|
242
|
+
case 'apply': return runApply(input);
|
|
243
|
+
case 'show': return runShow(input);
|
|
244
|
+
case 'reset': return runReset(input);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Register the `peaks skill scope` subcommand on the `skill` command group.
|
|
249
|
+
* Mutually-exclusive flags: exactly one of --detect / --apply / --show / --reset.
|
|
250
|
+
*/
|
|
251
|
+
export function registerSkillScopeCommands(program, io) {
|
|
252
|
+
// Find the existing 'skill' subcommand if any.
|
|
253
|
+
let skillCmd = program.commands.find((c) => c.name() === 'skill');
|
|
254
|
+
if (skillCmd === undefined) {
|
|
255
|
+
skillCmd = program.command('skill').description('Manage Peaks skills');
|
|
256
|
+
}
|
|
257
|
+
const scope = skillCmd
|
|
258
|
+
.command('scope')
|
|
259
|
+
.description('Per-project skill scoping: detect, apply, show, reset');
|
|
260
|
+
addJsonOption(scope
|
|
261
|
+
.option('--detect', 'dry-run: print the relevance matrix')
|
|
262
|
+
.option('--apply', 'apply the scope (writes source-of-truth + IDE config)')
|
|
263
|
+
.option('--show', 'show the currently applied scope')
|
|
264
|
+
.option('--reset', 'remove the scope config')
|
|
265
|
+
.option('--project <path>', 'target project root (defaults to cwd)', process.cwd())
|
|
266
|
+
.option('--strict', '--apply: only `relevant` skills in the allowlist')
|
|
267
|
+
.option('--loose', '--apply: `relevant` + `borderline` in the allowlist (default)')
|
|
268
|
+
.option('--ide <name>', 'force a specific IDE adapter (overrides auto-detect)')
|
|
269
|
+
// R003.3: --shadow-fallback is the new default. Pass --no-shadow-fallback
|
|
270
|
+
// to opt out (copy the full SKILL.md to .claude/skills/ for IDE-side inspection).
|
|
271
|
+
.option('--shadow-fallback', '--apply: Claude Code uses shadow stubs for the denylist (default)')
|
|
272
|
+
.option('--no-shadow-fallback', '--apply: copy the full SKILL.md for the denylist (opt out of shadowing)')).action(async (options) => {
|
|
273
|
+
const flags = [options.detect, options.apply, options.show, options.reset].filter(Boolean).length;
|
|
274
|
+
if (flags !== 1) {
|
|
275
|
+
const envelope = fail('skill.scope', 'INVALID_USAGE', 'Exactly one of --detect / --apply / --show / --reset is required', null, ['Pass exactly one action flag']);
|
|
276
|
+
printResult(io, envelope, options.json === true);
|
|
277
|
+
process.exitCode = 2;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const subcommand = options.detect
|
|
281
|
+
? 'detect'
|
|
282
|
+
: options.apply
|
|
283
|
+
? 'apply'
|
|
284
|
+
: options.show
|
|
285
|
+
? 'show'
|
|
286
|
+
: 'reset';
|
|
287
|
+
const result = await runSkillScopeCommand({
|
|
288
|
+
subcommand,
|
|
289
|
+
project: options.project ?? process.cwd(),
|
|
290
|
+
...(options.strict !== undefined ? { strict: options.strict } : {}),
|
|
291
|
+
...(options.loose !== undefined ? { loose: options.loose } : {}),
|
|
292
|
+
...(options.ide !== undefined ? { ide: options.ide } : {}),
|
|
293
|
+
...(options.shadowFallback !== undefined ? { shadowFallback: options.shadowFallback } : {}),
|
|
294
|
+
...(options.json !== undefined ? { json: options.json } : {}),
|
|
295
|
+
});
|
|
296
|
+
if (options.json === true) {
|
|
297
|
+
if (result.envelope !== null)
|
|
298
|
+
printResult(io, result.envelope, true);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
if (result.stdout.length > 0)
|
|
302
|
+
io.stdout(result.stdout);
|
|
303
|
+
if (result.stderr.length > 0)
|
|
304
|
+
io.stderr(result.stderr);
|
|
305
|
+
}
|
|
306
|
+
if (result.exitCode !== 0) {
|
|
307
|
+
process.exitCode = result.exitCode;
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|