peaks-cli 1.4.2 → 2.0.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/.claude-plugin/marketplace.json +51 -0
- package/CHANGELOG.md +238 -0
- package/README-en.md +226 -0
- package/README.md +152 -122
- package/dist/src/cli/commands/agent-commands.d.ts +20 -0
- package/dist/src/cli/commands/agent-commands.js +48 -0
- package/dist/src/cli/commands/audit-commands.d.ts +18 -0
- package/dist/src/cli/commands/audit-commands.js +138 -0
- package/dist/src/cli/commands/classify-classify-commands.d.ts +19 -0
- package/dist/src/cli/commands/classify-classify-commands.js +151 -0
- package/dist/src/cli/commands/code-review-commands.d.ts +34 -0
- package/dist/src/cli/commands/code-review-commands.js +83 -0
- package/dist/src/cli/commands/config-commands.js +90 -0
- package/dist/src/cli/commands/context-commands.d.ts +21 -0
- package/dist/src/cli/commands/context-commands.js +167 -0
- package/dist/src/cli/commands/core-artifact-commands.js +60 -2
- package/dist/src/cli/commands/hook-handle.js +50 -0
- package/dist/src/cli/commands/loop-commands.d.ts +21 -0
- package/dist/src/cli/commands/loop-commands.js +128 -0
- package/dist/src/cli/commands/openspec-commands.js +37 -0
- package/dist/src/cli/commands/preferences-commands.d.ts +2 -0
- package/dist/src/cli/commands/preferences-commands.js +147 -0
- package/dist/src/cli/commands/skill-conformance-commands.d.ts +9 -0
- package/dist/src/cli/commands/skill-conformance-commands.js +39 -0
- package/dist/src/cli/commands/understand-commands.js +34 -0
- package/dist/src/cli/commands/upgrade-commands.d.ts +23 -0
- package/dist/src/cli/commands/upgrade-commands.js +57 -0
- package/dist/src/cli/commands/workflow-commands.js +70 -0
- package/dist/src/cli/commands/workspace-commands.js +86 -0
- package/dist/src/cli/program.js +30 -0
- package/dist/src/services/agent/ecc-agent-service.d.ts +47 -0
- package/dist/src/services/agent/ecc-agent-service.js +143 -0
- package/dist/src/services/artifacts/request-artifact-service.js +14 -0
- package/dist/src/services/audit/backing-detector.d.ts +24 -0
- package/dist/src/services/audit/backing-detector.js +59 -0
- package/dist/src/services/audit/classifier.d.ts +38 -0
- package/dist/src/services/audit/classifier.js +127 -0
- package/dist/src/services/audit/enforcers/active-skill-resolver.d.ts +29 -0
- package/dist/src/services/audit/enforcers/active-skill-resolver.js +71 -0
- package/dist/src/services/audit/enforcers/design-draft-confirm.d.ts +25 -0
- package/dist/src/services/audit/enforcers/design-draft-confirm.js +54 -0
- package/dist/src/services/audit/enforcers/lint-audit-regression.d.ts +21 -0
- package/dist/src/services/audit/enforcers/lint-audit-regression.js +86 -0
- package/dist/src/services/audit/enforcers/lint-catalog-governance.d.ts +27 -0
- package/dist/src/services/audit/enforcers/lint-catalog-governance.js +38 -0
- package/dist/src/services/audit/enforcers/lint-cli-back.d.ts +16 -0
- package/dist/src/services/audit/enforcers/lint-cli-back.js +35 -0
- package/dist/src/services/audit/enforcers/lint-output-style.d.ts +11 -0
- package/dist/src/services/audit/enforcers/lint-output-style.js +94 -0
- package/dist/src/services/audit/enforcers/lint-reference-integrity.d.ts +6 -0
- package/dist/src/services/audit/enforcers/lint-reference-integrity.js +83 -0
- package/dist/src/services/audit/enforcers/lint-reference-shape.d.ts +30 -0
- package/dist/src/services/audit/enforcers/lint-reference-shape.js +272 -0
- package/dist/src/services/audit/enforcers/lint-style.d.ts +49 -0
- package/dist/src/services/audit/enforcers/lint-style.js +173 -0
- package/dist/src/services/audit/enforcers/lint-workflow-shape.d.ts +5 -0
- package/dist/src/services/audit/enforcers/lint-workflow-shape.js +141 -0
- package/dist/src/services/audit/enforcers/login-gate.d.ts +23 -0
- package/dist/src/services/audit/enforcers/login-gate.js +40 -0
- package/dist/src/services/audit/enforcers/mock-placement.d.ts +25 -0
- package/dist/src/services/audit/enforcers/mock-placement.js +48 -0
- package/dist/src/services/audit/enforcers/no-root-pollution.d.ts +21 -0
- package/dist/src/services/audit/enforcers/no-root-pollution.js +56 -0
- package/dist/src/services/audit/enforcers/pre-rd-scan.d.ts +22 -0
- package/dist/src/services/audit/enforcers/pre-rd-scan.js +23 -0
- package/dist/src/services/audit/enforcers/prototype-fidelity.d.ts +25 -0
- package/dist/src/services/audit/enforcers/prototype-fidelity.js +75 -0
- package/dist/src/services/audit/enforcers/resume-detection.d.ts +21 -0
- package/dist/src/services/audit/enforcers/resume-detection.js +52 -0
- package/dist/src/services/audit/enforcers/solo-code-ban.d.ts +23 -0
- package/dist/src/services/audit/enforcers/solo-code-ban.js +27 -0
- package/dist/src/services/audit/enforcers/sub-agent-sid.d.ts +25 -0
- package/dist/src/services/audit/enforcers/sub-agent-sid.js +63 -0
- package/dist/src/services/audit/enforcers/tech-doc-presence.d.ts +28 -0
- package/dist/src/services/audit/enforcers/tech-doc-presence.js +35 -0
- package/dist/src/services/audit/red-line-catalog-p2-a.d.ts +21 -0
- package/dist/src/services/audit/red-line-catalog-p2-a.js +233 -0
- package/dist/src/services/audit/red-line-catalog-p2-b.d.ts +19 -0
- package/dist/src/services/audit/red-line-catalog-p2-b.js +225 -0
- package/dist/src/services/audit/red-line-catalog.d.ts +51 -0
- package/dist/src/services/audit/red-line-catalog.js +210 -0
- package/dist/src/services/audit/red-lines-service.d.ts +23 -0
- package/dist/src/services/audit/red-lines-service.js +486 -0
- package/dist/src/services/audit/scanners/openspec-scanner.d.ts +15 -0
- package/dist/src/services/audit/scanners/openspec-scanner.js +55 -0
- package/dist/src/services/audit/scanners/rules-tree-scanner.d.ts +16 -0
- package/dist/src/services/audit/scanners/rules-tree-scanner.js +56 -0
- package/dist/src/services/audit/scanners/skills-tree-scanner.d.ts +17 -0
- package/dist/src/services/audit/scanners/skills-tree-scanner.js +46 -0
- package/dist/src/services/audit/static-service.d.ts +57 -0
- package/dist/src/services/audit/static-service.js +125 -0
- package/dist/src/services/audit/types.d.ts +69 -0
- package/dist/src/services/audit/types.js +13 -0
- package/dist/src/services/classify/classify-service.d.ts +42 -0
- package/dist/src/services/classify/classify-service.js +122 -0
- package/dist/src/services/classify/classify-types.d.ts +79 -0
- package/dist/src/services/classify/classify-types.js +90 -0
- package/dist/src/services/code-review/ocr-service.d.ts +129 -0
- package/dist/src/services/code-review/ocr-service.js +362 -0
- package/dist/src/services/config/config-migration.d.ts +32 -0
- package/dist/src/services/config/config-migration.js +92 -0
- package/dist/src/services/config/config-restore.d.ts +10 -0
- package/dist/src/services/config/config-restore.js +47 -0
- package/dist/src/services/config/config-rollback.d.ts +13 -0
- package/dist/src/services/config/config-rollback.js +26 -0
- package/dist/src/services/config/config-service.d.ts +35 -2
- package/dist/src/services/config/config-service.js +81 -0
- package/dist/src/services/config/config-types.d.ts +58 -0
- package/dist/src/services/config/config-types.js +6 -0
- package/dist/src/services/doctor/doctor-service.js +96 -0
- package/dist/src/services/ide/adapters/hermes-adapter.d.ts +21 -0
- package/dist/src/services/ide/adapters/hermes-adapter.js +51 -0
- package/dist/src/services/ide/adapters/openclaw-adapter.d.ts +14 -0
- package/dist/src/services/ide/adapters/openclaw-adapter.js +42 -0
- package/dist/src/services/ide/ide-registry.js +7 -0
- package/dist/src/services/ide/ide-types.d.ts +1 -1
- package/dist/src/services/openspec/openspec-propose-from-doctor-service.d.ts +31 -0
- package/dist/src/services/openspec/openspec-propose-from-doctor-service.js +95 -0
- package/dist/src/services/preferences/preferences-service.d.ts +6 -0
- package/dist/src/services/preferences/preferences-service.js +43 -0
- package/dist/src/services/preferences/preferences-types.d.ts +90 -0
- package/dist/src/services/preferences/preferences-types.js +38 -0
- package/dist/src/services/skills/skill-conformance-service.d.ts +40 -0
- package/dist/src/services/skills/skill-conformance-service.js +136 -0
- package/dist/src/services/skills/skill-runbook-service.js +44 -10
- package/dist/src/services/skills/sync-service.d.ts +43 -0
- package/dist/src/services/skills/sync-service.js +99 -0
- package/dist/src/services/slice/slice-check-service.js +166 -13
- package/dist/src/services/slice/slice-check-types.d.ts +1 -1
- package/dist/src/services/standards/migrate-claude-rules-service.d.ts +19 -0
- package/dist/src/services/standards/migrate-claude-rules-service.js +193 -0
- package/dist/src/services/understand/understand-scan-service.js +15 -2
- package/dist/src/services/understand/understand-types.d.ts +26 -0
- package/dist/src/services/upgrade/1x-detector-service.d.ts +7 -0
- package/dist/src/services/upgrade/1x-detector-service.js +94 -0
- package/dist/src/services/upgrade/gitignore-migrate-service.d.ts +56 -0
- package/dist/src/services/upgrade/gitignore-migrate-service.js +170 -0
- package/dist/src/services/upgrade/upgrade-service.d.ts +47 -0
- package/dist/src/services/upgrade/upgrade-service.js +381 -0
- package/dist/src/services/workspace/sid-naming-guard.d.ts +14 -0
- package/dist/src/services/workspace/sid-naming-guard.js +31 -0
- package/dist/src/services/workspace/workspace-archive-service.d.ts +19 -0
- package/dist/src/services/workspace/workspace-archive-service.js +32 -0
- package/dist/src/services/workspace/workspace-clean-service.d.ts +41 -0
- package/dist/src/services/workspace/workspace-clean-service.js +86 -0
- package/dist/src/services/workspace/workspace-state-service.d.ts +7 -0
- package/dist/src/services/workspace/workspace-state-service.js +43 -0
- package/dist/src/shared/change-id.js +4 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +8 -2
- package/schemas/doctor-report.schema.json +1 -1
- package/scripts/install-skills.mjs +296 -12
- package/skills/peaks-doctor/SKILL.md +59 -0
- package/skills/peaks-doctor/references/doctor-check-catalog.md +31 -0
- package/skills/peaks-doctor/references/from-doctor-flow.md +64 -0
- package/skills/peaks-doctor/test_prompts.json +17 -0
- package/skills/peaks-ide/SKILL.md +2 -0
- package/skills/peaks-qa/SKILL.md +9 -7
- package/skills/peaks-qa/references/artifact-per-request.md +19 -5
- package/skills/peaks-qa/references/qa-perf-test-plan.md +6 -6
- package/skills/peaks-qa/references/qa-runbook.md +1 -1
- package/skills/peaks-rd/SKILL.md +25 -10
- package/skills/peaks-rd/references/ocr-integration.md +214 -0
- package/skills/peaks-rd/references/rd-fanout-contracts.md +70 -0
- package/skills/peaks-rd/references/rd-runbook.md +1 -1
- package/skills/peaks-solo/SKILL.md +10 -4
- package/skills/peaks-solo/references/step-0-55-1x-detection.md +82 -0
- package/skills/peaks-solo/references/workflow-gates-and-types.md +9 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openspec-propose-from-doctor-service (Slice L3.3) — generates a draft
|
|
3
|
+
* OpenSpec change record (proposal.md) from a doctor CRITICAL finding.
|
|
4
|
+
*
|
|
5
|
+
* Per L1+L2+L3 redesign §5.4: "CRITICAL → proposal 草稿生成". The doctor
|
|
6
|
+
* scans the project for issues; when a CRITICAL finding is surfaced,
|
|
7
|
+
* peaks-cli generates a draft `openspec/changes/<id>/proposal.md` so the
|
|
8
|
+
* LLM doesn't have to start from scratch.
|
|
9
|
+
*
|
|
10
|
+
* The draft proposal is INFORMATIONAL — it requires the LLM to review +
|
|
11
|
+
* edit before `peaks openspec validate` will accept it.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { getErrorMessage } from '../../shared/result.js';
|
|
16
|
+
const SLUGIFY = /[^a-z0-9]+/g;
|
|
17
|
+
function slugify(text) {
|
|
18
|
+
return text
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(SLUGIFY, '-')
|
|
21
|
+
.replace(/^-+|-+$/g, '')
|
|
22
|
+
.slice(0, 50);
|
|
23
|
+
}
|
|
24
|
+
function formatProposal(input) {
|
|
25
|
+
return `# Doctor Finding: ${input.finding.rule}
|
|
26
|
+
|
|
27
|
+
**Date**: ${input.date}
|
|
28
|
+
**Change ID**: ${input.changeId}
|
|
29
|
+
**Source**: \`peaks doctor\` finding (CRITICAL severity: ${input.finding.severity})
|
|
30
|
+
|
|
31
|
+
## Why
|
|
32
|
+
|
|
33
|
+
A \`peaks doctor\` scan flagged the following issue at \`${input.finding.id}\`:
|
|
34
|
+
|
|
35
|
+
> ${input.finding.detail}
|
|
36
|
+
|
|
37
|
+
The current behavior is broken or degraded. This proposal outlines a fix.
|
|
38
|
+
|
|
39
|
+
## What Changes
|
|
40
|
+
|
|
41
|
+
- Address the doctor finding at \`${input.finding.id}\`.
|
|
42
|
+
- See the Why section above for the original error message.
|
|
43
|
+
- Acceptance criteria below describe the success conditions.
|
|
44
|
+
|
|
45
|
+
## Acceptance Criteria
|
|
46
|
+
|
|
47
|
+
- \`peaks doctor --json\` returns \`ok: true\` for the \`${input.finding.id}\` check.
|
|
48
|
+
- Re-running the audit does not regress other findings.
|
|
49
|
+
|
|
50
|
+
## Out of Scope
|
|
51
|
+
|
|
52
|
+
- Other doctor findings (each is tracked in its own OpenSpec change).
|
|
53
|
+
- Refactors that don't fix this specific issue.
|
|
54
|
+
|
|
55
|
+
## Risks
|
|
56
|
+
|
|
57
|
+
- Low: this is a doctor-flagged issue with a clear acceptance criterion.
|
|
58
|
+
|
|
59
|
+
## Status
|
|
60
|
+
|
|
61
|
+
- created: ${input.date}
|
|
62
|
+
- last update: ${input.date}
|
|
63
|
+
- state: draft
|
|
64
|
+
- state reason: auto-generated from peaks doctor; LLM must review + edit before validate
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
function makeChangeId(finding, date) {
|
|
68
|
+
return `${date}-fix-${slugify(finding.id)}`;
|
|
69
|
+
}
|
|
70
|
+
export function proposeFromDoctor(input) {
|
|
71
|
+
const date = (input.clock ?? (() => new Date().toISOString()))().slice(0, 10);
|
|
72
|
+
const changeId = makeChangeId(input.finding, date);
|
|
73
|
+
const changeDir = join(input.projectRoot, 'openspec/changes', changeId);
|
|
74
|
+
const proposalPath = join(changeDir, 'proposal.md');
|
|
75
|
+
let created = false;
|
|
76
|
+
try {
|
|
77
|
+
if (!existsSync(changeDir)) {
|
|
78
|
+
mkdirSync(changeDir, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
if (!existsSync(proposalPath)) {
|
|
81
|
+
const content = formatProposal({
|
|
82
|
+
changeId,
|
|
83
|
+
finding: input.finding,
|
|
84
|
+
title: `Fix ${input.finding.rule}`,
|
|
85
|
+
date,
|
|
86
|
+
});
|
|
87
|
+
writeFileSync(proposalPath, content);
|
|
88
|
+
created = true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
throw new Error(`proposeFromDoctor: ${getErrorMessage(error)}`);
|
|
93
|
+
}
|
|
94
|
+
return { changeId, changeDir, proposalPath, created };
|
|
95
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { DEFAULT_PREFERENCES, PREFERENCES_SCHEMA_VERSION, type ProjectPreferences } from './preferences-types.js';
|
|
2
|
+
export { DEFAULT_PREFERENCES, PREFERENCES_SCHEMA_VERSION };
|
|
3
|
+
export type { ProjectPreferences } from './preferences-types.js';
|
|
4
|
+
export declare function preferencesPath(projectRoot: string): string;
|
|
5
|
+
export declare function loadPreferences(projectRoot: string): ProjectPreferences;
|
|
6
|
+
export declare function savePreferences(projectRoot: string, overrides: Partial<ProjectPreferences>): ProjectPreferences;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { DEFAULT_PREFERENCES, PREFERENCES_SCHEMA_VERSION, } from './preferences-types.js';
|
|
4
|
+
const PREFS_REL_PATH = '.peaks/preferences.json';
|
|
5
|
+
export { DEFAULT_PREFERENCES, PREFERENCES_SCHEMA_VERSION };
|
|
6
|
+
export function preferencesPath(projectRoot) {
|
|
7
|
+
return join(projectRoot, PREFS_REL_PATH);
|
|
8
|
+
}
|
|
9
|
+
export function loadPreferences(projectRoot) {
|
|
10
|
+
const filePath = preferencesPath(projectRoot);
|
|
11
|
+
if (!existsSync(filePath)) {
|
|
12
|
+
return structuredClone(DEFAULT_PREFERENCES);
|
|
13
|
+
}
|
|
14
|
+
let raw;
|
|
15
|
+
try {
|
|
16
|
+
raw = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
throw new Error(`PREFERENCES_JSON_INVALID: failed to parse ${filePath}: ${err.message}`);
|
|
20
|
+
}
|
|
21
|
+
if (typeof raw !== 'object' ||
|
|
22
|
+
raw === null ||
|
|
23
|
+
raw.schema_version !== PREFERENCES_SCHEMA_VERSION) {
|
|
24
|
+
throw new Error(`PREFERENCES_SCHEMA_MISMATCH: expected schema_version=${PREFERENCES_SCHEMA_VERSION} in ${filePath}, got ${raw?.schema_version}`);
|
|
25
|
+
}
|
|
26
|
+
return mergePreferences(DEFAULT_PREFERENCES, raw);
|
|
27
|
+
}
|
|
28
|
+
export function savePreferences(projectRoot, overrides) {
|
|
29
|
+
const filePath = preferencesPath(projectRoot);
|
|
30
|
+
const current = loadPreferences(projectRoot);
|
|
31
|
+
const merged = mergePreferences(current, overrides);
|
|
32
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
33
|
+
writeFileSync(filePath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
|
|
34
|
+
return merged;
|
|
35
|
+
}
|
|
36
|
+
function mergePreferences(base, overrides) {
|
|
37
|
+
const definedEntries = Object.entries(overrides).filter(([, value]) => value !== undefined);
|
|
38
|
+
return {
|
|
39
|
+
...base,
|
|
40
|
+
...Object.fromEntries(definedEntries),
|
|
41
|
+
schema_version: PREFERENCES_SCHEMA_VERSION,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peaks-cli 2.0 project-local preferences schema.
|
|
3
|
+
* Per spec §8.4 — per-project state lives in `.peaks/preferences.json`,
|
|
4
|
+
* NOT in `~/.peaks/config.json` (which is slim global).
|
|
5
|
+
*
|
|
6
|
+
* Spec reference: docs/superpowers/specs/2026-06-11-peaks-cli-l1-l2-l3-redesign.md §8.4
|
|
7
|
+
*/
|
|
8
|
+
export declare const PREFERENCES_SCHEMA_VERSION = "2.0.0";
|
|
9
|
+
/**
|
|
10
|
+
* Per-task-level UA install UX prompt decision.
|
|
11
|
+
* Values:
|
|
12
|
+
* - 'unset' — ask every session (default)
|
|
13
|
+
* - 'skip-this-session' — skip prompt for current session only
|
|
14
|
+
* - 'skip-forever' — never ask, never install
|
|
15
|
+
*/
|
|
16
|
+
export type UaPromptDecision = 'unset' | 'skip-this-session' | 'skip-forever';
|
|
17
|
+
/**
|
|
18
|
+
* L1a task classification conservatism.
|
|
19
|
+
* Values:
|
|
20
|
+
* - 'default' — use default signal thresholds
|
|
21
|
+
* - 'strict' — always upgrade to next level (slower, safer)
|
|
22
|
+
* - 'lax' — always downgrade to previous level (faster, riskier)
|
|
23
|
+
*/
|
|
24
|
+
export type ClassifyConservatism = 'default' | 'strict' | 'lax';
|
|
25
|
+
/**
|
|
26
|
+
* Per-touchpoint headroom-AI mode override.
|
|
27
|
+
* Spec §7.4 — default 'balanced'.
|
|
28
|
+
*/
|
|
29
|
+
export type HeadroomMode = 'balanced' | 'aggressive' | 'conservative';
|
|
30
|
+
export interface HeadroomPreferences {
|
|
31
|
+
/** Whether headroom integration is enabled globally. Default: true */
|
|
32
|
+
readonly enabled: boolean;
|
|
33
|
+
/** Default mode if a touchpoint doesn't override. Default: 'balanced' */
|
|
34
|
+
readonly defaultMode: HeadroomMode;
|
|
35
|
+
/** Per-touchpoint mode overrides */
|
|
36
|
+
readonly perTouchpoint: {
|
|
37
|
+
memorySearch: HeadroomMode;
|
|
38
|
+
retrospectiveSearch: HeadroomMode;
|
|
39
|
+
doctorScan: HeadroomMode;
|
|
40
|
+
doctorRoute: HeadroomMode;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export interface ClassifyRuleOverrides {
|
|
44
|
+
/** File count threshold above which a task is promoted to 'feature' */
|
|
45
|
+
readonly feature_threshold_files?: number;
|
|
46
|
+
/** Line count threshold above which a task is promoted to 'feature' */
|
|
47
|
+
readonly feature_threshold_lines?: number;
|
|
48
|
+
/** Whether to require a 24h grace before cleaning recently-active sessions */
|
|
49
|
+
readonly runtime_clean_grace_hours?: number;
|
|
50
|
+
}
|
|
51
|
+
export interface SwarmSpeculativePreferences {
|
|
52
|
+
/** Whether speculative dispatch is enabled. Default: true */
|
|
53
|
+
readonly enabled: boolean;
|
|
54
|
+
/** Max concurrent speculative sub-agents. Default: 2 */
|
|
55
|
+
readonly maxConcurrent: number;
|
|
56
|
+
/** Min hit rate below which speculative auto-disables. Default: 0.5 */
|
|
57
|
+
readonly minHitRate: number;
|
|
58
|
+
}
|
|
59
|
+
export interface ProjectPreferences {
|
|
60
|
+
/**
|
|
61
|
+
* On-disk schema version. The JSON key is `schema_version` (snake_case) — this matches the
|
|
62
|
+
* raw on-disk key in `.peaks/preferences.json`, NOT the camelCase used by the rest of this
|
|
63
|
+
* interface. preferences-service.ts validates this value against PREFERENCES_SCHEMA_VERSION
|
|
64
|
+
* on load and writes the current value on save. Any mismatch throws PREFERENCES_SCHEMA_MISMATCH.
|
|
65
|
+
*/
|
|
66
|
+
readonly schema_version: typeof PREFERENCES_SCHEMA_VERSION;
|
|
67
|
+
readonly economyMode: boolean;
|
|
68
|
+
readonly swarmMode: boolean;
|
|
69
|
+
readonly uaPrompt: UaPromptDecision;
|
|
70
|
+
readonly agentShieldPrompt: UaPromptDecision;
|
|
71
|
+
readonly classifyConservatism: ClassifyConservatism;
|
|
72
|
+
readonly classifyRules: ClassifyRuleOverrides;
|
|
73
|
+
readonly headroom: HeadroomPreferences;
|
|
74
|
+
readonly swarmSpeculative: SwarmSpeculativePreferences;
|
|
75
|
+
/** Loop Autonomous (L4 14.5) toggle. Default: false — never auto-enable. */
|
|
76
|
+
readonly loopAutonomousEnabled: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* L2.3 P2-a: ECC AgentShield subprocess toggle. Default: false.
|
|
79
|
+
*
|
|
80
|
+
* When true, `peaks audit static` spawns `npx ecc-agentshield scan --json`
|
|
81
|
+
* and merges its findings into the audit report. When false (default),
|
|
82
|
+
* the audit runs peaks-cli-only and the subprocess is never spawned.
|
|
83
|
+
*
|
|
84
|
+
* The preference is independent of whether ECC is installed — i.e.
|
|
85
|
+
* `agentShieldEnabled: true` with ECC missing surfaces a soft
|
|
86
|
+
* "ECC not installed" warning and the audit still completes.
|
|
87
|
+
*/
|
|
88
|
+
readonly agentShieldEnabled: boolean;
|
|
89
|
+
}
|
|
90
|
+
export declare const DEFAULT_PREFERENCES: ProjectPreferences;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peaks-cli 2.0 project-local preferences schema.
|
|
3
|
+
* Per spec §8.4 — per-project state lives in `.peaks/preferences.json`,
|
|
4
|
+
* NOT in `~/.peaks/config.json` (which is slim global).
|
|
5
|
+
*
|
|
6
|
+
* Spec reference: docs/superpowers/specs/2026-06-11-peaks-cli-l1-l2-l3-redesign.md §8.4
|
|
7
|
+
*/
|
|
8
|
+
export const PREFERENCES_SCHEMA_VERSION = '2.0.0';
|
|
9
|
+
export const DEFAULT_PREFERENCES = {
|
|
10
|
+
schema_version: PREFERENCES_SCHEMA_VERSION,
|
|
11
|
+
economyMode: true,
|
|
12
|
+
swarmMode: true,
|
|
13
|
+
uaPrompt: 'unset',
|
|
14
|
+
agentShieldPrompt: 'unset',
|
|
15
|
+
classifyConservatism: 'default',
|
|
16
|
+
classifyRules: {
|
|
17
|
+
feature_threshold_files: 10,
|
|
18
|
+
feature_threshold_lines: 100,
|
|
19
|
+
runtime_clean_grace_hours: 24,
|
|
20
|
+
},
|
|
21
|
+
headroom: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
defaultMode: 'balanced',
|
|
24
|
+
perTouchpoint: {
|
|
25
|
+
memorySearch: 'balanced',
|
|
26
|
+
retrospectiveSearch: 'balanced',
|
|
27
|
+
doctorScan: 'balanced',
|
|
28
|
+
doctorRoute: 'conservative',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
swarmSpeculative: {
|
|
32
|
+
enabled: true,
|
|
33
|
+
maxConcurrent: 2,
|
|
34
|
+
minHitRate: 0.5,
|
|
35
|
+
},
|
|
36
|
+
loopAutonomousEnabled: false,
|
|
37
|
+
agentShieldEnabled: false,
|
|
38
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill-conformance audit (Slice #12) — checks every peaks-* SKILL.md
|
|
3
|
+
* against the 5 standards from the L1+L2+L3 redesign §5.4 Slice #12:
|
|
4
|
+
*
|
|
5
|
+
* 1. task-level frontmatter (all 12 skills must declare one)
|
|
6
|
+
* 2. CLI-back 注解 coverage (each skill body must document which
|
|
7
|
+
* `peaks <cmd>` commands it composes; the absence is a SKILL.md
|
|
8
|
+
* anti-pattern — it means the LLM has to discover the CLI
|
|
9
|
+
* primitives by accident)
|
|
10
|
+
* 3. loadStrategy on-demand 标注 (skills should declare when they
|
|
11
|
+
* load — `eager` for always-loaded, `on-demand` for invoked)
|
|
12
|
+
* 4. 800-line cap (Karpathy limit per spec §2.3)
|
|
13
|
+
* 5. outputStyle: peaks-concise-v1 frontmatter (peak-cli display
|
|
14
|
+
* style for the skill's user-visible output)
|
|
15
|
+
*
|
|
16
|
+
* Plus 1 derived check:
|
|
17
|
+
* 6. CLI primitives declared in references/audit/ (Skill must
|
|
18
|
+
* surface every peaks <cmd> it composes in the references/ subdir;
|
|
19
|
+
* this is the "CLI-back 注解 100% 覆盖" check)
|
|
20
|
+
*/
|
|
21
|
+
export type LoadStrategy = 'eager' | 'on-demand';
|
|
22
|
+
export type ConformanceLevel = 'pass' | 'warn' | 'fail';
|
|
23
|
+
export interface ConformanceCheck {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly skill: string;
|
|
26
|
+
readonly level: ConformanceLevel;
|
|
27
|
+
readonly message: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ConformanceReport {
|
|
30
|
+
readonly checked: number;
|
|
31
|
+
readonly passed: number;
|
|
32
|
+
readonly warned: number;
|
|
33
|
+
readonly failed: number;
|
|
34
|
+
readonly checks: readonly ConformanceCheck[];
|
|
35
|
+
readonly summary: string;
|
|
36
|
+
}
|
|
37
|
+
export interface SkillConformanceInput {
|
|
38
|
+
readonly projectRoot: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function auditSkillConformance(input: SkillConformanceInput): ConformanceReport;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill-conformance audit (Slice #12) — checks every peaks-* SKILL.md
|
|
3
|
+
* against the 5 standards from the L1+L2+L3 redesign §5.4 Slice #12:
|
|
4
|
+
*
|
|
5
|
+
* 1. task-level frontmatter (all 12 skills must declare one)
|
|
6
|
+
* 2. CLI-back 注解 coverage (each skill body must document which
|
|
7
|
+
* `peaks <cmd>` commands it composes; the absence is a SKILL.md
|
|
8
|
+
* anti-pattern — it means the LLM has to discover the CLI
|
|
9
|
+
* primitives by accident)
|
|
10
|
+
* 3. loadStrategy on-demand 标注 (skills should declare when they
|
|
11
|
+
* load — `eager` for always-loaded, `on-demand` for invoked)
|
|
12
|
+
* 4. 800-line cap (Karpathy limit per spec §2.3)
|
|
13
|
+
* 5. outputStyle: peaks-concise-v1 frontmatter (peak-cli display
|
|
14
|
+
* style for the skill's user-visible output)
|
|
15
|
+
*
|
|
16
|
+
* Plus 1 derived check:
|
|
17
|
+
* 6. CLI primitives declared in references/audit/ (Skill must
|
|
18
|
+
* surface every peaks <cmd> it composes in the references/ subdir;
|
|
19
|
+
* this is the "CLI-back 注解 100% 覆盖" check)
|
|
20
|
+
*/
|
|
21
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
22
|
+
import { existsSync } from 'node:fs';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
const SKILL_NAMES = [
|
|
25
|
+
'peaks-ide',
|
|
26
|
+
'peaks-prd',
|
|
27
|
+
'peaks-ui',
|
|
28
|
+
'peaks-rd',
|
|
29
|
+
'peaks-qa',
|
|
30
|
+
'peaks-sc',
|
|
31
|
+
'peaks-solo',
|
|
32
|
+
'peaks-solo-resume',
|
|
33
|
+
'peaks-solo-status',
|
|
34
|
+
'peaks-solo-test',
|
|
35
|
+
'peaks-sop',
|
|
36
|
+
'peaks-txt',
|
|
37
|
+
'peaks-doctor'
|
|
38
|
+
];
|
|
39
|
+
const REQUIRED_FRONTMATTER_FIELDS = ['name', 'description'];
|
|
40
|
+
const OPTIONAL_FRONTMATTER_FIELDS = ['task-level', 'loadStrategy', 'outputStyle'];
|
|
41
|
+
const MAX_LINE_COUNT = 800;
|
|
42
|
+
const CLI_BACK_PATTERNS = [
|
|
43
|
+
/`peaks\s+[a-z][a-z0-9-]+(?:\s+[a-z][a-z0-9-]+)*`/g,
|
|
44
|
+
/`peaks\s+[a-z][a-z0-9-]+/g,
|
|
45
|
+
];
|
|
46
|
+
function readSkillFrontmatter(skillPath) {
|
|
47
|
+
if (!existsSync(skillPath))
|
|
48
|
+
return null;
|
|
49
|
+
const content = readFileSync(skillPath, 'utf-8');
|
|
50
|
+
const match = /^---\n([\s\S]*?)\n---\n/.exec(content);
|
|
51
|
+
if (match === null)
|
|
52
|
+
return { raw: content, fields: {} };
|
|
53
|
+
const raw = match[1] ?? '';
|
|
54
|
+
const fields = {};
|
|
55
|
+
for (const line of raw.split('\n')) {
|
|
56
|
+
const colonIdx = line.indexOf(':');
|
|
57
|
+
if (colonIdx === -1)
|
|
58
|
+
continue;
|
|
59
|
+
const key = line.slice(0, colonIdx).trim();
|
|
60
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
61
|
+
if (key.length > 0)
|
|
62
|
+
fields[key] = value;
|
|
63
|
+
}
|
|
64
|
+
return { raw, fields };
|
|
65
|
+
}
|
|
66
|
+
function lineCount(filePath) {
|
|
67
|
+
try {
|
|
68
|
+
return statSync(filePath).size > 0 ? readFileSync(filePath, 'utf-8').split(/\r?\n/).length : 0;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function countCliBackReferences(content) {
|
|
75
|
+
let count = 0;
|
|
76
|
+
for (const pattern of CLI_BACK_PATTERNS) {
|
|
77
|
+
pattern.lastIndex = 0;
|
|
78
|
+
const matches = content.match(pattern);
|
|
79
|
+
if (matches)
|
|
80
|
+
count += matches.length;
|
|
81
|
+
}
|
|
82
|
+
return count;
|
|
83
|
+
}
|
|
84
|
+
export function auditSkillConformance(input) {
|
|
85
|
+
const skillsDir = join(input.projectRoot, 'skills');
|
|
86
|
+
const checks = [];
|
|
87
|
+
for (const skillName of SKILL_NAMES) {
|
|
88
|
+
const skillPath = join(skillsDir, skillName, 'SKILL.md');
|
|
89
|
+
const fm = readSkillFrontmatter(skillPath);
|
|
90
|
+
// 1. task-level frontmatter
|
|
91
|
+
if (fm === null) {
|
|
92
|
+
checks.push({ id: 'frontmatter:present', skill: skillName, level: 'fail', message: 'SKILL.md missing' });
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
for (const required of REQUIRED_FRONTMATTER_FIELDS) {
|
|
96
|
+
if (fm.fields[required] === undefined) {
|
|
97
|
+
checks.push({ id: `frontmatter:${required}`, skill: skillName, level: 'fail', message: `frontmatter missing required field "${required}"` });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// 2. CLI-back annotation
|
|
102
|
+
if (fm !== null) {
|
|
103
|
+
const content = readFileSync(skillPath, 'utf-8');
|
|
104
|
+
const cliCount = countCliBackReferences(content);
|
|
105
|
+
if (cliCount === 0) {
|
|
106
|
+
checks.push({ id: 'cli-back:present', skill: skillName, level: 'warn', message: 'no `peaks <cmd>` references in SKILL.md body; consider documenting which CLI primitives the skill composes' });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// 3. loadStrategy on-demand 标注
|
|
110
|
+
if (fm !== null && fm.fields['loadStrategy'] === undefined) {
|
|
111
|
+
checks.push({ id: 'loadStrategy:declared', skill: skillName, level: 'warn', message: 'loadStrategy not declared in frontmatter (eager | on-demand)' });
|
|
112
|
+
}
|
|
113
|
+
// 4. 800-line cap
|
|
114
|
+
if (fm !== null) {
|
|
115
|
+
const lines = lineCount(skillPath);
|
|
116
|
+
if (lines > MAX_LINE_COUNT) {
|
|
117
|
+
checks.push({ id: 'line-count:cap', skill: skillName, level: 'fail', message: `${lines} lines > ${MAX_LINE_COUNT} cap (Karpathy)` });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// 5. outputStyle: peaks-concise-v1
|
|
121
|
+
if (fm !== null && fm.fields['outputStyle'] === undefined) {
|
|
122
|
+
checks.push({ id: 'outputStyle:declared', skill: skillName, level: 'warn', message: 'outputStyle not declared in frontmatter' });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const failed = checks.filter((c) => c.level === 'fail').length;
|
|
126
|
+
const warned = checks.filter((c) => c.level === 'warn').length;
|
|
127
|
+
const passed = checks.filter((c) => c.level === 'pass').length;
|
|
128
|
+
return {
|
|
129
|
+
checked: checks.length,
|
|
130
|
+
passed,
|
|
131
|
+
warned,
|
|
132
|
+
failed,
|
|
133
|
+
checks,
|
|
134
|
+
summary: failed === 0 ? 'all hard checks pass; warnings are advisory' : `${failed} hard failure(s); fix before shipping`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dirname, join } from 'node:path';
|
|
2
|
+
import * as path from 'node:path';
|
|
2
3
|
import { readText } from '../../shared/fs.js';
|
|
3
4
|
import { loadSkillRegistry } from './skill-registry.js';
|
|
4
5
|
const DESTRUCTIVE_APPLY_PATTERNS = [
|
|
@@ -12,8 +13,25 @@ const DESTRUCTIVE_APPLY_PATTERNS = [
|
|
|
12
13
|
const AUTHORIZATION_KEYWORDS_PATTERN = /authoriz|explicit|--dry-run|approv|only after|only when/i;
|
|
13
14
|
const PEAKS_COMMAND_LINE_PATTERN = /^\s*peaks\s+\w/;
|
|
14
15
|
function extractRunbookSection(body) {
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
// Find a `## Default runbook` heading at the start of a line, then capture
|
|
17
|
+
// the section body up to the next `## ` heading or end of input.
|
|
18
|
+
//
|
|
19
|
+
// Implementation note: we avoid a regex with a multiline `(?=\n## |$)`
|
|
20
|
+
// lookahead because the `m` flag turns `$` into "end of any line" in
|
|
21
|
+
// JavaScript, which would let the lazy capture stop at the first `\n`.
|
|
22
|
+
// Instead, we (a) use the regex only to locate the heading, then (b)
|
|
23
|
+
// manually scan forward for the next `## ` heading or end of input.
|
|
24
|
+
const headingRe = /^## Default runbook[^\n]*(?:\n|$)/m;
|
|
25
|
+
const headingMatch = headingRe.exec(body);
|
|
26
|
+
if (headingMatch === null)
|
|
27
|
+
return null;
|
|
28
|
+
const startAfter = headingMatch.index + headingMatch[0].length;
|
|
29
|
+
const rest = body.slice(startAfter);
|
|
30
|
+
// Find the next `## ` heading at the start of a line.
|
|
31
|
+
const nextHeadingRe = /^## /m;
|
|
32
|
+
const nextMatch = nextHeadingRe.exec(rest);
|
|
33
|
+
const end = nextMatch === null ? rest.length : nextMatch.index;
|
|
34
|
+
return rest.slice(0, end);
|
|
17
35
|
}
|
|
18
36
|
/**
|
|
19
37
|
* Load the runbook section, falling back to `references/runbook.md` if the
|
|
@@ -32,15 +50,31 @@ function extractRunbookSection(body) {
|
|
|
32
50
|
*/
|
|
33
51
|
async function loadRunbookSection(skillPath, body) {
|
|
34
52
|
const inline = extractRunbookSection(body);
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
const skillDir = dirname(skillPath);
|
|
54
|
+
// Try multiple reference filenames. Convention: each skill may name its
|
|
55
|
+
// runbook file `runbook.md` (the generic name, used by peaks-solo), or
|
|
56
|
+
// `<role>-runbook.md` (the role-suffixed name, used by peaks-rd → rd-runbook.md,
|
|
57
|
+
// peaks-qa → qa-runbook.md, etc.). Prefer the longest extracted section.
|
|
58
|
+
const refCandidates = [
|
|
59
|
+
join(skillDir, 'references', 'runbook.md'),
|
|
60
|
+
join(skillDir, 'references', `${path.basename(skillDir).replace(/^peaks-/, '')}-runbook.md`)
|
|
61
|
+
];
|
|
62
|
+
let bestRef = null;
|
|
63
|
+
for (const refPath of refCandidates) {
|
|
64
|
+
try {
|
|
65
|
+
const refBody = await readText(refPath);
|
|
66
|
+
const refSection = extractRunbookSection(refBody);
|
|
67
|
+
if (refSection === null)
|
|
68
|
+
continue;
|
|
69
|
+
if (bestRef === null || refSection.length > bestRef.length) {
|
|
70
|
+
bestRef = refSection;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// candidate not present or unreadable; try the next one
|
|
75
|
+
}
|
|
43
76
|
}
|
|
77
|
+
const refSection = bestRef;
|
|
44
78
|
if (inline === null)
|
|
45
79
|
return refSection;
|
|
46
80
|
if (refSection === null)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { IdeId } from '../ide/ide-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The 8 platforms per Slice #12 final piece. Slice #0.7 + Slice
|
|
4
|
+
* #0.5.2 registered these in the IdeId union; this list is the
|
|
5
|
+
* single source of truth for the sync fan-out.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SYNC_PLATFORMS: readonly IdeId[];
|
|
8
|
+
export interface PlatformSyncResult {
|
|
9
|
+
/** The platform that was attempted. */
|
|
10
|
+
readonly platform: IdeId;
|
|
11
|
+
/** True if installBundledSkills returned without error. */
|
|
12
|
+
readonly ok: boolean;
|
|
13
|
+
/** Skills newly symlinked (idempotent re-runs return []). */
|
|
14
|
+
readonly installed: readonly string[];
|
|
15
|
+
/** Skills whose target was not a managed symlink (third-party owned). */
|
|
16
|
+
readonly skipped: readonly string[];
|
|
17
|
+
/** Error message; present when ok=false. */
|
|
18
|
+
readonly error?: string;
|
|
19
|
+
/** Wall-clock duration in ms. */
|
|
20
|
+
readonly durationMs: number;
|
|
21
|
+
}
|
|
22
|
+
export interface SyncServiceInput {
|
|
23
|
+
readonly projectRoot: string;
|
|
24
|
+
/** When omitted, the service iterates all 8 platforms. */
|
|
25
|
+
readonly platforms?: readonly IdeId[] | undefined;
|
|
26
|
+
/** When true, the installer is invoked in dry-run mode. */
|
|
27
|
+
readonly dryRun?: boolean | undefined;
|
|
28
|
+
}
|
|
29
|
+
export interface SyncServiceResult {
|
|
30
|
+
readonly applied: boolean;
|
|
31
|
+
readonly dryRun: boolean;
|
|
32
|
+
readonly projectRoot: string;
|
|
33
|
+
readonly perPlatform: readonly PlatformSyncResult[];
|
|
34
|
+
readonly syncedCount: number;
|
|
35
|
+
readonly failedCount: number;
|
|
36
|
+
readonly totalInstalled: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Validate a single platform id against the SYNC_PLATFORMS
|
|
40
|
+
* allowlist. Throws on a bogus value.
|
|
41
|
+
*/
|
|
42
|
+
export declare function assertValidPlatform(platform: string): asserts platform is IdeId;
|
|
43
|
+
export declare function runSkillSync(input: SyncServiceInput): Promise<SyncServiceResult>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peaks skill sync — fan-out the peaks-* skill family to all 8
|
|
3
|
+
* supported LLM-CLI platforms. Per spec §9 line 1105 (Slice #12
|
|
4
|
+
* final piece): "peaks skills sync 8 平台分发".
|
|
5
|
+
*
|
|
6
|
+
* The 8 target platforms are enumerated by the `IdeId` union
|
|
7
|
+
* (src/services/ide/ide-types.ts:16-24). The per-IDE install
|
|
8
|
+
* profile is `IdeSkillInstall`; the actual symlink installer
|
|
9
|
+
* is `scripts/install-skills.mjs::installBundledSkills` (dynamically
|
|
10
|
+
* imported so this module does not require a build step).
|
|
11
|
+
*/
|
|
12
|
+
import { pathToFileURL } from 'node:url';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
/**
|
|
15
|
+
* The 8 platforms per Slice #12 final piece. Slice #0.7 + Slice
|
|
16
|
+
* #0.5.2 registered these in the IdeId union; this list is the
|
|
17
|
+
* single source of truth for the sync fan-out.
|
|
18
|
+
*/
|
|
19
|
+
export const SYNC_PLATFORMS = [
|
|
20
|
+
'claude-code',
|
|
21
|
+
'trae',
|
|
22
|
+
'codex',
|
|
23
|
+
'cursor',
|
|
24
|
+
'qoder',
|
|
25
|
+
'tongyi-lingma',
|
|
26
|
+
'hermes',
|
|
27
|
+
'openclaw',
|
|
28
|
+
];
|
|
29
|
+
let cachedInstaller = null;
|
|
30
|
+
async function loadInstaller() {
|
|
31
|
+
if (cachedInstaller !== null)
|
|
32
|
+
return cachedInstaller;
|
|
33
|
+
const scriptPath = join(process.cwd(), 'scripts', 'install-skills.mjs');
|
|
34
|
+
const mod = (await import(pathToFileURL(scriptPath).href));
|
|
35
|
+
cachedInstaller = mod.installBundledSkills;
|
|
36
|
+
return cachedInstaller;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Validate a single platform id against the SYNC_PLATFORMS
|
|
40
|
+
* allowlist. Throws on a bogus value.
|
|
41
|
+
*/
|
|
42
|
+
export function assertValidPlatform(platform) {
|
|
43
|
+
if (!SYNC_PLATFORMS.includes(platform)) {
|
|
44
|
+
throw new Error(`peaks skill sync: unknown platform "${platform}". ` +
|
|
45
|
+
`Valid platforms: ${SYNC_PLATFORMS.join(', ')}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function runSkillSync(input) {
|
|
49
|
+
const platforms = input.platforms ?? SYNC_PLATFORMS;
|
|
50
|
+
for (const p of platforms) {
|
|
51
|
+
assertValidPlatform(p);
|
|
52
|
+
}
|
|
53
|
+
const dryRun = input.dryRun === true;
|
|
54
|
+
const installer = await loadInstaller();
|
|
55
|
+
const perPlatform = [];
|
|
56
|
+
let syncedCount = 0;
|
|
57
|
+
let failedCount = 0;
|
|
58
|
+
let totalInstalled = 0;
|
|
59
|
+
for (const platform of platforms) {
|
|
60
|
+
const start = Date.now();
|
|
61
|
+
try {
|
|
62
|
+
const result = installer({
|
|
63
|
+
ideId: platform,
|
|
64
|
+
projectRoot: input.projectRoot,
|
|
65
|
+
...(dryRun ? { dryRun: true } : {}),
|
|
66
|
+
});
|
|
67
|
+
perPlatform.push({
|
|
68
|
+
platform,
|
|
69
|
+
ok: true,
|
|
70
|
+
installed: result.installed,
|
|
71
|
+
skipped: result.skipped,
|
|
72
|
+
durationMs: Date.now() - start,
|
|
73
|
+
});
|
|
74
|
+
syncedCount += 1;
|
|
75
|
+
totalInstalled += result.installed.length;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
79
|
+
perPlatform.push({
|
|
80
|
+
platform,
|
|
81
|
+
ok: false,
|
|
82
|
+
installed: [],
|
|
83
|
+
skipped: [],
|
|
84
|
+
error: message,
|
|
85
|
+
durationMs: Date.now() - start,
|
|
86
|
+
});
|
|
87
|
+
failedCount += 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
applied: !dryRun,
|
|
92
|
+
dryRun,
|
|
93
|
+
projectRoot: input.projectRoot,
|
|
94
|
+
perPlatform,
|
|
95
|
+
syncedCount,
|
|
96
|
+
failedCount,
|
|
97
|
+
totalInstalled,
|
|
98
|
+
};
|
|
99
|
+
}
|