peaks-cli 1.0.2 → 1.0.3
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/bin/peaks.js +0 -0
- package/dist/src/cli/commands/config-commands.js +3 -2
- package/dist/src/cli/commands/workflow-commands.js +13 -2
- package/dist/src/services/artifacts/artifact-service.js +5 -4
- package/dist/src/services/artifacts/workspace-service.d.ts +1 -0
- package/dist/src/services/artifacts/workspace-service.js +56 -28
- package/dist/src/services/config/config-service.d.ts +2 -0
- package/dist/src/services/config/config-service.js +139 -12
- package/dist/src/services/config/config-types.d.ts +16 -5
- package/dist/src/services/sc/sc-service.js +32 -15
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/output-styles/peaks-skill-swarm.md +132 -0
- package/package.json +2 -1
- package/scripts/install-skills.mjs +65 -8
- package/skills/peaks-prd/SKILL.md +8 -0
- package/skills/peaks-qa/SKILL.md +12 -0
- package/skills/peaks-rd/SKILL.md +49 -1
- package/skills/peaks-sc/SKILL.md +8 -0
- package/skills/peaks-solo/SKILL.md +28 -0
- package/skills/peaks-txt/SKILL.md +12 -0
- package/skills/peaks-ui/SKILL.md +8 -0
package/bin/peaks.js
CHANGED
|
File without changes
|
|
@@ -144,7 +144,8 @@ function registerWorkspaceCommands(config, io) {
|
|
|
144
144
|
const artifactRepo = parseArtifactRepoInput(io, options, options.json);
|
|
145
145
|
if (artifactRepo === null)
|
|
146
146
|
return;
|
|
147
|
-
const
|
|
147
|
+
const artifactStorage = artifactRepo ? { mode: 'local-with-remote-sync', remote: artifactRepo } : { mode: 'local' };
|
|
148
|
+
const workspace = { workspaceId: options.id, name: options.name, rootPath: options.path, installedCapabilityIds: [], artifactStorage };
|
|
148
149
|
const configLayer = layer ?? 'user';
|
|
149
150
|
if (artifactRepo) {
|
|
150
151
|
addWorkspace({ ...workspace, artifactRepo }, configLayer);
|
|
@@ -152,7 +153,7 @@ function registerWorkspaceCommands(config, io) {
|
|
|
152
153
|
else {
|
|
153
154
|
addWorkspace(workspace, configLayer);
|
|
154
155
|
}
|
|
155
|
-
printResult(io, ok('config.workspace.add', { workspaceId: options.id, name: options.name, rootPath: options.path, artifactRepo }), options.json);
|
|
156
|
+
printResult(io, ok('config.workspace.add', { workspaceId: options.id, name: options.name, rootPath: options.path, artifactRepo, artifactStorage }), options.json);
|
|
156
157
|
});
|
|
157
158
|
addJsonOption(configWorkspace.command('remove').description('Remove a workspace').requiredOption('--id <id>', 'workspace identifier').option('--layer <layer>', 'user or project')).action((options) => {
|
|
158
159
|
const layer = parseConfigLayer(options.layer);
|
|
@@ -5,6 +5,7 @@ import { createAutonomousWorkflowPlan } from '../../services/workflow/workflow-a
|
|
|
5
5
|
import { createRecommendationPlan } from '../../services/recommendations/recommendation-service.js';
|
|
6
6
|
import { createRefactorDryRun } from '../../services/refactor/refactor-service.js';
|
|
7
7
|
import { getCurrentWorkspaceConfig, readConfig } from '../../services/config/config-service.js';
|
|
8
|
+
import { validateChangeIdOrThrow } from '../../shared/change-id.js';
|
|
8
9
|
import { getEconomyAwareExecutionModelId } from '../../services/config/model-routing.js';
|
|
9
10
|
import { getLocalArtifactPath } from '../../services/artifacts/workspace-service.js';
|
|
10
11
|
import { fail, ok } from '../../shared/result.js';
|
|
@@ -24,6 +25,12 @@ function parseMaxWorkers(io, command, value, asJson) {
|
|
|
24
25
|
}
|
|
25
26
|
return maxWorkers;
|
|
26
27
|
}
|
|
28
|
+
function validatePlanningInput(changeId, goal) {
|
|
29
|
+
validateChangeIdOrThrow(changeId);
|
|
30
|
+
if (!goal.trim()) {
|
|
31
|
+
throw new Error('Goal must be non-empty');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
27
34
|
function parseSoloMode(io, command, mode, soloMode, asJson) {
|
|
28
35
|
if (mode !== 'solo' && soloMode) {
|
|
29
36
|
printResult(io, fail(command, 'SOLO_MODE_REQUIRES_SOLO_WORKFLOW', '--solo-mode can only be used with --mode solo', {}, ['Remove --solo-mode or use --mode solo']), asJson);
|
|
@@ -46,6 +53,7 @@ function runTechPlan(io, options) {
|
|
|
46
53
|
return;
|
|
47
54
|
}
|
|
48
55
|
try {
|
|
56
|
+
validatePlanningInput(options.changeId, options.goal);
|
|
49
57
|
const workspaceContext = getWorkspaceContext();
|
|
50
58
|
const plan = createTechPlan({
|
|
51
59
|
changeId: options.changeId,
|
|
@@ -88,6 +96,7 @@ function runWorkflowRoute(io, options) {
|
|
|
88
96
|
if (soloMode === null)
|
|
89
97
|
return;
|
|
90
98
|
try {
|
|
99
|
+
validatePlanningInput(options.changeId, options.goal);
|
|
91
100
|
const workspaceContext = getWorkspaceContext();
|
|
92
101
|
const plan = createWorkflowRouterPlan({
|
|
93
102
|
changeId: options.changeId,
|
|
@@ -123,6 +132,7 @@ function runAutonomousWorkflow(io, options) {
|
|
|
123
132
|
if (soloMode === null)
|
|
124
133
|
return;
|
|
125
134
|
try {
|
|
135
|
+
validatePlanningInput(options.changeId, options.goal);
|
|
126
136
|
const workspaceContext = getWorkspaceContext();
|
|
127
137
|
const plan = createAutonomousWorkflowPlan({
|
|
128
138
|
changeId: options.changeId,
|
|
@@ -155,6 +165,7 @@ function runSwarmPlan(io, options) {
|
|
|
155
165
|
if (maxWorkers === null)
|
|
156
166
|
return;
|
|
157
167
|
try {
|
|
168
|
+
validatePlanningInput(options.changeId, options.goal);
|
|
158
169
|
const workspaceContext = getWorkspaceContext();
|
|
159
170
|
const config = readConfig();
|
|
160
171
|
const plan = createRdSwarmPlan({
|
|
@@ -246,12 +257,12 @@ export function registerWorkflowCommands(program, io) {
|
|
|
246
257
|
.command('recommend')
|
|
247
258
|
.description('Create a dry-run recommendation plan for a workflow')
|
|
248
259
|
.requiredOption('--workflow <workflow>', 'workflow: code-refactor, product-refactor, or frontend-design')
|
|
249
|
-
.option('--language <language>', 'human presentation language'
|
|
260
|
+
.option('--language <language>', 'human presentation language')).action((options) => {
|
|
250
261
|
if (!isRecommendationWorkflow(options.workflow)) {
|
|
251
262
|
printResult(io, fail('recommend', 'UNSUPPORTED_RECOMMENDATION_WORKFLOW', `Unsupported recommendation workflow ${options.workflow}`, {}, ['Use --workflow code-refactor, product-refactor, or frontend-design']), options.json);
|
|
252
263
|
process.exitCode = 1;
|
|
253
264
|
return;
|
|
254
265
|
}
|
|
255
|
-
printResult(io, ok('recommend', createRecommendationPlan({ workflow: options.workflow, language: options.language })), options.json);
|
|
266
|
+
printResult(io, ok('recommend', createRecommendationPlan({ workflow: options.workflow, language: options.language ?? readConfig().language })), options.json);
|
|
256
267
|
});
|
|
257
268
|
}
|
|
@@ -3,7 +3,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
import { getCurrentWorkspaceConfig } from '../config/config-service.js';
|
|
6
|
-
import { getLocalArtifactPath } from './workspace-service.js';
|
|
6
|
+
import { getArtifactRemoteRepo, getLocalArtifactPath } from './workspace-service.js';
|
|
7
7
|
function getRemoteUrl(artifactRepo) {
|
|
8
8
|
if (!artifactRepo)
|
|
9
9
|
return null;
|
|
@@ -46,7 +46,7 @@ export function createArtifactInitPlan(options) {
|
|
|
46
46
|
}
|
|
47
47
|
export function createGuidedArtifactSetup() {
|
|
48
48
|
const workspace = getCurrentWorkspaceConfig();
|
|
49
|
-
const artifactRepo = workspace
|
|
49
|
+
const artifactRepo = workspace ? getArtifactRemoteRepo(workspace) : null;
|
|
50
50
|
const validationResult = {
|
|
51
51
|
workspaceExists: workspace !== null,
|
|
52
52
|
gitAvailable: hasGit(),
|
|
@@ -65,7 +65,7 @@ export function createGuidedArtifactSetup() {
|
|
|
65
65
|
localPath,
|
|
66
66
|
remoteUrl,
|
|
67
67
|
validationResult,
|
|
68
|
-
nextStep: workspace ? (artifactRepo ? 'validate' : '
|
|
68
|
+
nextStep: workspace ? (artifactRepo ? 'validate' : 'complete') : 'configure',
|
|
69
69
|
guidance: [
|
|
70
70
|
'Step 1: Detect current workspace and environment',
|
|
71
71
|
` - Workspace: ${workspace?.workspaceId ?? 'not configured'}`,
|
|
@@ -74,6 +74,7 @@ export function createGuidedArtifactSetup() {
|
|
|
74
74
|
` - SSH key for code push: ${validationResult.sshKeyAvailable ? 'available' : 'not found'}`,
|
|
75
75
|
'',
|
|
76
76
|
'Step 2: Configure artifact repository',
|
|
77
|
+
' - Optional for local-only storage',
|
|
77
78
|
' - Run: peaks artifacts init --provider github --name <repo> --dry-run',
|
|
78
79
|
' - Or add to workspace: peaks config workspace add --id <id> --provider github --repo-owner <owner> --repo-name <name>',
|
|
79
80
|
'',
|
|
@@ -82,7 +83,7 @@ export function createGuidedArtifactSetup() {
|
|
|
82
83
|
' - Run: peaks artifacts workspace',
|
|
83
84
|
'',
|
|
84
85
|
'Step 4: Complete',
|
|
85
|
-
' - Artifact sync is ready when workspace has
|
|
86
|
+
artifactRepo ? ' - Artifact sync is ready when workspace has remote artifact storage configured' : ' - Local artifact storage is ready'
|
|
86
87
|
]
|
|
87
88
|
};
|
|
88
89
|
}
|
|
@@ -22,6 +22,7 @@ export type SyncResult = {
|
|
|
22
22
|
export declare function getLocalArtifactPath(workspace: WorkspaceConfig): string;
|
|
23
23
|
export declare function isArtifactWorkspaceOutsideTarget(workspace: WorkspaceConfig, artifactWorkspacePath?: string): boolean;
|
|
24
24
|
export declare function hasValidArtifactWorkspace(workspace: WorkspaceConfig, artifactWorkspacePath?: string): boolean;
|
|
25
|
+
export declare function getArtifactRemoteRepo(workspace: WorkspaceConfig): WorkspaceConfig['artifactRepo'] | null;
|
|
25
26
|
export declare function executeArtifactSync(workspaceId?: string): Promise<SyncResult>;
|
|
26
27
|
export declare function getArtifactWorkspaceStatus(workspaceId?: string): ArtifactWorkspaceStatus;
|
|
27
28
|
export declare function planArtifactSync(workspaceId?: string, dryRun?: boolean): {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { Buffer } from 'node:buffer';
|
|
3
|
-
import {
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
4
5
|
import { isInsidePath, stablePath } from '../../shared/path-utils.js';
|
|
5
6
|
import { readConfig, getCurrentWorkspaceConfig } from '../config/config-service.js';
|
|
6
7
|
import { pathExists } from '../../shared/fs.js';
|
|
@@ -12,8 +13,10 @@ function canonicalChildPath(parentPath, ...segments) {
|
|
|
12
13
|
return stablePath(resolve(parentPath, ...segments));
|
|
13
14
|
}
|
|
14
15
|
export function getLocalArtifactPath(workspace) {
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
if (workspace.artifactStorage?.localPath) {
|
|
17
|
+
return resolve(workspace.artifactStorage.localPath);
|
|
18
|
+
}
|
|
19
|
+
return resolve(homedir(), '.peaks', 'workspaces', workspace.workspaceId, 'artifacts');
|
|
17
20
|
}
|
|
18
21
|
export function isArtifactWorkspaceOutsideTarget(workspace, artifactWorkspacePath = getLocalArtifactPath(workspace)) {
|
|
19
22
|
const targetRoot = canonicalPath(workspace.rootPath);
|
|
@@ -44,6 +47,15 @@ export function hasValidArtifactWorkspace(workspace, artifactWorkspacePath = get
|
|
|
44
47
|
return false;
|
|
45
48
|
return true;
|
|
46
49
|
}
|
|
50
|
+
export function getArtifactRemoteRepo(workspace) {
|
|
51
|
+
if (workspace.artifactStorage?.mode === 'local-with-remote-sync') {
|
|
52
|
+
return workspace.artifactStorage.remote;
|
|
53
|
+
}
|
|
54
|
+
if (workspace.artifactStorage?.mode === 'local') {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return workspace.artifactRepo ?? null;
|
|
58
|
+
}
|
|
47
59
|
function getPublicRemoteUrl(artifactRepo) {
|
|
48
60
|
if (!artifactRepo)
|
|
49
61
|
return null;
|
|
@@ -78,7 +90,7 @@ export async function executeArtifactSync(workspaceId) {
|
|
|
78
90
|
const workspace = workspaceId
|
|
79
91
|
? readConfig().workspaces.find((w) => w.workspaceId === workspaceId) ?? null
|
|
80
92
|
: getCurrentWorkspaceConfig();
|
|
81
|
-
if (!workspace
|
|
93
|
+
if (!workspace) {
|
|
82
94
|
return {
|
|
83
95
|
workspaceId: workspaceId ?? 'unknown',
|
|
84
96
|
success: false,
|
|
@@ -86,7 +98,7 @@ export async function executeArtifactSync(workspaceId) {
|
|
|
86
98
|
remoteUrl: null,
|
|
87
99
|
commands: [],
|
|
88
100
|
output: [],
|
|
89
|
-
error: '
|
|
101
|
+
error: 'Workspace not found'
|
|
90
102
|
};
|
|
91
103
|
}
|
|
92
104
|
const localPath = getLocalArtifactPath(workspace);
|
|
@@ -101,8 +113,19 @@ export async function executeArtifactSync(workspaceId) {
|
|
|
101
113
|
error: 'Artifact workspace must be outside the target repository'
|
|
102
114
|
};
|
|
103
115
|
}
|
|
104
|
-
const
|
|
105
|
-
|
|
116
|
+
const artifactRepo = getArtifactRemoteRepo(workspace);
|
|
117
|
+
if (!artifactRepo) {
|
|
118
|
+
return {
|
|
119
|
+
workspaceId: workspace.workspaceId,
|
|
120
|
+
success: true,
|
|
121
|
+
localPath,
|
|
122
|
+
remoteUrl: null,
|
|
123
|
+
commands: [],
|
|
124
|
+
output: ['Local artifact storage is configured']
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const remoteUrl = getPublicRemoteUrl(artifactRepo);
|
|
128
|
+
const gitAuthEnv = getGitAuthEnv(artifactRepo);
|
|
106
129
|
if (!remoteUrl) {
|
|
107
130
|
return {
|
|
108
131
|
workspaceId: workspace.workspaceId,
|
|
@@ -183,9 +206,9 @@ export function getArtifactWorkspaceStatus(workspaceId) {
|
|
|
183
206
|
}
|
|
184
207
|
const localPath = getLocalArtifactPath(workspace);
|
|
185
208
|
const hasLocalDir = existsSync(localPath);
|
|
186
|
-
const
|
|
209
|
+
const artifactRepo = getArtifactRemoteRepo(workspace);
|
|
187
210
|
const hasSafeBoundary = isArtifactWorkspaceOutsideTarget(workspace, localPath);
|
|
188
|
-
const syncStatus = !
|
|
211
|
+
const syncStatus = !hasSafeBoundary
|
|
189
212
|
? 'unknown'
|
|
190
213
|
: !hasLocalDir
|
|
191
214
|
? 'pending'
|
|
@@ -193,23 +216,23 @@ export function getArtifactWorkspaceStatus(workspaceId) {
|
|
|
193
216
|
return {
|
|
194
217
|
workspaceId: workspace.workspaceId,
|
|
195
218
|
localPath,
|
|
196
|
-
configured:
|
|
219
|
+
configured: hasSafeBoundary,
|
|
197
220
|
syncStatus,
|
|
198
221
|
lastSync: null,
|
|
199
222
|
hasLocalChanges: false,
|
|
200
|
-
artifactRepo
|
|
223
|
+
artifactRepo,
|
|
201
224
|
nextActions: !hasSafeBoundary
|
|
202
225
|
? ['Configure artifact workspace outside the target repository.']
|
|
203
|
-
:
|
|
226
|
+
: artifactRepo
|
|
204
227
|
? [`Run peaks artifacts sync --workspace ${workspace.workspaceId} --dry-run`]
|
|
205
|
-
: [`
|
|
228
|
+
: [`Local artifact storage ready at ${localPath}`]
|
|
206
229
|
};
|
|
207
230
|
}
|
|
208
231
|
export function planArtifactSync(workspaceId, dryRun = true) {
|
|
209
232
|
const workspace = workspaceId
|
|
210
233
|
? readConfig().workspaces.find((w) => w.workspaceId === workspaceId) ?? null
|
|
211
234
|
: getCurrentWorkspaceConfig();
|
|
212
|
-
if (!workspace
|
|
235
|
+
if (!workspace) {
|
|
213
236
|
return {
|
|
214
237
|
workspaceId: workspaceId ?? 'unknown',
|
|
215
238
|
dryRun,
|
|
@@ -228,21 +251,26 @@ export function planArtifactSync(workspaceId, dryRun = true) {
|
|
|
228
251
|
plannedCommands: ['Artifact workspace must be outside the target repository']
|
|
229
252
|
};
|
|
230
253
|
}
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
254
|
+
const artifactRepo = getArtifactRemoteRepo(workspace);
|
|
255
|
+
const remoteUrl = getPublicRemoteUrl(artifactRepo);
|
|
256
|
+
const plannedCommands = artifactRepo
|
|
257
|
+
? dryRun
|
|
258
|
+
? [
|
|
259
|
+
`# Sync plan for workspace ${workspace.workspaceId}`,
|
|
260
|
+
`# Local: ${localPath}`,
|
|
261
|
+
`# Remote: ${remoteUrl}`,
|
|
262
|
+
'# peaks artifacts sync --workspace ' + workspace.workspaceId,
|
|
263
|
+
'# (dry-run only — no changes made)'
|
|
264
|
+
]
|
|
265
|
+
: [
|
|
266
|
+
`# Sync execution for workspace ${workspace.workspaceId}`,
|
|
267
|
+
`# Confirm: will sync ${localPath} with ${remoteUrl}`,
|
|
268
|
+
'# Exit 1 if not confirmed'
|
|
269
|
+
]
|
|
242
270
|
: [
|
|
243
|
-
`#
|
|
244
|
-
`#
|
|
245
|
-
'#
|
|
271
|
+
`# Local artifact storage for workspace ${workspace.workspaceId}`,
|
|
272
|
+
`# Local: ${localPath}`,
|
|
273
|
+
'# No remote repository is configured or required'
|
|
246
274
|
];
|
|
247
275
|
return {
|
|
248
276
|
workspaceId: workspace.workspaceId,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ConfigGetOptions, ConfigLayer, ConfigSetOptions, MiniMaxProviderConfig, PeaksConfig, TokenRef, WorkspaceConfig } from './config-types.js';
|
|
2
|
+
export declare function resolveProjectRootForConfig(startPath: string): string;
|
|
2
3
|
export declare function isConfigLayer(value: string): value is ConfigLayer;
|
|
3
4
|
export declare function isSensitiveConfigPath(path: string): boolean;
|
|
4
5
|
export declare function containsSensitiveConfigValue(value: unknown): boolean;
|
|
@@ -17,6 +18,7 @@ export type MiniMaxProviderStatus = {
|
|
|
17
18
|
export declare function getMiniMaxProviderConfig(): MiniMaxProviderConfig;
|
|
18
19
|
export declare function getMiniMaxProviderStatus(): MiniMaxProviderStatus;
|
|
19
20
|
export declare function setMiniMaxProviderConfig(input: MiniMaxProviderConfig): MiniMaxProviderStatus;
|
|
21
|
+
export declare function bootstrapProjectLanguageConfig(projectRoot: string, language: string): void;
|
|
20
22
|
export declare function readConfig(projectRoot?: string | null): PeaksConfig;
|
|
21
23
|
export declare function writeConfig(partial: Partial<PeaksConfig>, layer?: ConfigLayer): void;
|
|
22
24
|
export declare function getConfig(options?: ConfigGetOptions): unknown;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { DEFAULT_CONFIG } from './config-types.js';
|
|
@@ -38,6 +38,23 @@ function findProjectRoot(startPath) {
|
|
|
38
38
|
}
|
|
39
39
|
return null;
|
|
40
40
|
}
|
|
41
|
+
export function resolveProjectRootForConfig(startPath) {
|
|
42
|
+
const start = resolve(startPath);
|
|
43
|
+
const homePath = resolve(homedir());
|
|
44
|
+
let current = start;
|
|
45
|
+
let parent = dirname(current);
|
|
46
|
+
while (current !== parent && current !== homePath) {
|
|
47
|
+
if (existsSync(resolve(current, '.peaks', 'config.json')) && isSafeProjectConfigMarker(current)) {
|
|
48
|
+
return current;
|
|
49
|
+
}
|
|
50
|
+
if (existsSync(resolve(current, 'package.json')) || existsSync(resolve(current, '.git'))) {
|
|
51
|
+
return current;
|
|
52
|
+
}
|
|
53
|
+
parent = current;
|
|
54
|
+
current = dirname(parent);
|
|
55
|
+
}
|
|
56
|
+
return start;
|
|
57
|
+
}
|
|
41
58
|
function getProjectConfigPath(projectRoot) {
|
|
42
59
|
if (!projectRoot)
|
|
43
60
|
return null;
|
|
@@ -45,6 +62,46 @@ function getProjectConfigPath(projectRoot) {
|
|
|
45
62
|
return null;
|
|
46
63
|
return resolve(projectRoot, '.peaks', 'config.json');
|
|
47
64
|
}
|
|
65
|
+
function getProjectBootstrapConfigPath(projectRoot) {
|
|
66
|
+
const projectRootPath = resolve(projectRoot);
|
|
67
|
+
const peaksPath = resolve(projectRootPath, '.peaks');
|
|
68
|
+
const configPath = resolve(peaksPath, 'config.json');
|
|
69
|
+
if (!isInsidePath(configPath, projectRootPath)) {
|
|
70
|
+
throw new Error('Project config path must stay inside the project root');
|
|
71
|
+
}
|
|
72
|
+
if (!existsSync(peaksPath)) {
|
|
73
|
+
mkdirSync(peaksPath, { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
validateProjectBootstrapConfigPath(projectRootPath, peaksPath, configPath);
|
|
76
|
+
return configPath;
|
|
77
|
+
}
|
|
78
|
+
function validateProjectBootstrapConfigPath(projectRootPath, peaksPath, configPath) {
|
|
79
|
+
const projectRootReal = realpathSync(projectRootPath);
|
|
80
|
+
const peaksStats = lstatSync(peaksPath);
|
|
81
|
+
const peaksReal = realpathSync(peaksPath);
|
|
82
|
+
if (!peaksStats.isDirectory() || peaksStats.isSymbolicLink() || peaksReal !== resolve(projectRootReal, '.peaks')) {
|
|
83
|
+
throw new Error('Project config path must stay inside the project root');
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const markerStats = lstatSync(configPath);
|
|
87
|
+
if (!markerStats.isFile() || markerStats.isSymbolicLink()) {
|
|
88
|
+
throw new Error('Project config path must stay inside the project root');
|
|
89
|
+
}
|
|
90
|
+
const markerReal = realpathSync(configPath);
|
|
91
|
+
if (!isInsidePath(markerReal, projectRootReal) || !isInsidePath(markerReal, peaksReal)) {
|
|
92
|
+
throw new Error('Project config path must stay inside the project root');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error.code !== 'ENOENT') {
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function validateProjectBootstrapConfigPathForWrite(projectRoot, configPath) {
|
|
102
|
+
const projectRootPath = resolve(projectRoot);
|
|
103
|
+
validateProjectBootstrapConfigPath(projectRootPath, resolve(projectRootPath, '.peaks'), configPath);
|
|
104
|
+
}
|
|
48
105
|
function readJsonFile(path) {
|
|
49
106
|
if (!path || !existsSync(path))
|
|
50
107
|
return null;
|
|
@@ -55,6 +112,16 @@ function readJsonFile(path) {
|
|
|
55
112
|
return null;
|
|
56
113
|
}
|
|
57
114
|
}
|
|
115
|
+
function readExistingJsonFile(path, errorMessage) {
|
|
116
|
+
if (!existsSync(path))
|
|
117
|
+
return null;
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
throw new Error(errorMessage);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
58
125
|
function ensureDir(dirPath) {
|
|
59
126
|
if (!existsSync(dirPath)) {
|
|
60
127
|
mkdirSync(dirPath, { recursive: true });
|
|
@@ -97,9 +164,9 @@ function setNestedValue(obj, path, value) {
|
|
|
97
164
|
const last = parts[parts.length - 1];
|
|
98
165
|
current[last] = value;
|
|
99
166
|
}
|
|
100
|
-
function
|
|
101
|
-
const { providers, ...safeConfig } = config;
|
|
102
|
-
return safeConfig;
|
|
167
|
+
function removeProjectSensitiveConfig(config) {
|
|
168
|
+
const { providers, proxy, tokens, ...safeConfig } = config;
|
|
169
|
+
return Object.fromEntries(Object.entries(safeConfig).filter(([key, value]) => !isSecretKey(key) && !containsSensitiveConfigValue(value)));
|
|
103
170
|
}
|
|
104
171
|
export function isConfigLayer(value) {
|
|
105
172
|
return value === 'user' || value === 'project';
|
|
@@ -214,18 +281,48 @@ function validateProxyConfig(partial) {
|
|
|
214
281
|
function isRecord(value) {
|
|
215
282
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
216
283
|
}
|
|
284
|
+
function isSafeConfigSegment(value) {
|
|
285
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value) && !value.includes('..') && !value.endsWith('.');
|
|
286
|
+
}
|
|
287
|
+
function toArtifactRemoteRepoConfig(value) {
|
|
288
|
+
if (!isRecord(value) || (value.provider !== 'github' && value.provider !== 'gitlab') || typeof value.owner !== 'string' || typeof value.name !== 'string') {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
if (!isSafeConfigSegment(value.owner) || !isSafeConfigSegment(value.name)) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
return { provider: value.provider, owner: value.owner, name: value.name };
|
|
295
|
+
}
|
|
296
|
+
function toArtifactStorageConfig(value) {
|
|
297
|
+
if (!isRecord(value))
|
|
298
|
+
return null;
|
|
299
|
+
const localPath = typeof value.localPath === 'string' ? { localPath: value.localPath } : {};
|
|
300
|
+
if (value.mode === 'local') {
|
|
301
|
+
return { mode: 'local', ...localPath };
|
|
302
|
+
}
|
|
303
|
+
const remote = toArtifactRemoteRepoConfig(value.remote);
|
|
304
|
+
if (value.mode === 'local-with-remote-sync' && remote) {
|
|
305
|
+
return { mode: 'local-with-remote-sync', ...localPath, remote };
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
217
309
|
function toWorkspaceConfig(value) {
|
|
218
310
|
if (!isRecord(value))
|
|
219
311
|
return null;
|
|
220
312
|
const { workspaceId, name, rootPath, installedCapabilityIds } = value;
|
|
221
|
-
if (typeof workspaceId !== 'string' || typeof name !== 'string' || typeof rootPath !== 'string' || !Array.isArray(installedCapabilityIds) || !installedCapabilityIds.every((id) => typeof id === 'string')) {
|
|
313
|
+
if (typeof workspaceId !== 'string' || !isSafeConfigSegment(workspaceId) || typeof name !== 'string' || typeof rootPath !== 'string' || !Array.isArray(installedCapabilityIds) || !installedCapabilityIds.every((id) => typeof id === 'string')) {
|
|
222
314
|
return null;
|
|
223
315
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
316
|
+
const artifactRepo = toArtifactRemoteRepoConfig(value.artifactRepo);
|
|
317
|
+
const artifactStorage = toArtifactStorageConfig(value.artifactStorage);
|
|
318
|
+
return {
|
|
319
|
+
workspaceId,
|
|
320
|
+
name,
|
|
321
|
+
rootPath,
|
|
322
|
+
installedCapabilityIds,
|
|
323
|
+
...(artifactRepo ? { artifactRepo } : {}),
|
|
324
|
+
...(artifactStorage ? { artifactStorage } : {})
|
|
325
|
+
};
|
|
229
326
|
}
|
|
230
327
|
function toWorkspaceConfigs(value) {
|
|
231
328
|
return Array.isArray(value) ? value.map(toWorkspaceConfig).filter((workspace) => workspace !== null) : [];
|
|
@@ -355,6 +452,19 @@ export function setMiniMaxProviderConfig(input) {
|
|
|
355
452
|
writeConfig({ providers }, 'user');
|
|
356
453
|
return createMiniMaxProviderStatus(providers.minimax ?? {});
|
|
357
454
|
}
|
|
455
|
+
function inferHumanLanguage(value) {
|
|
456
|
+
const normalized = value.trim();
|
|
457
|
+
if (!normalized) {
|
|
458
|
+
throw new Error('Language must be non-empty');
|
|
459
|
+
}
|
|
460
|
+
if (/^zh(?:-|$)/i.test(normalized) || /[㐀-鿿]/u.test(normalized)) {
|
|
461
|
+
return 'zh-CN';
|
|
462
|
+
}
|
|
463
|
+
if (/^en(?:-|$)/i.test(normalized)) {
|
|
464
|
+
return 'en';
|
|
465
|
+
}
|
|
466
|
+
return 'en';
|
|
467
|
+
}
|
|
358
468
|
function toPeaksConfig(value) {
|
|
359
469
|
if (!isRecord(value))
|
|
360
470
|
return {};
|
|
@@ -372,12 +482,22 @@ function toPeaksConfig(value) {
|
|
|
372
482
|
...(proxy ? { proxy } : {})
|
|
373
483
|
};
|
|
374
484
|
}
|
|
485
|
+
export function bootstrapProjectLanguageConfig(projectRoot, language) {
|
|
486
|
+
const inferredLanguage = inferHumanLanguage(language);
|
|
487
|
+
const projectPath = getProjectBootstrapConfigPath(projectRoot);
|
|
488
|
+
const existing = readExistingJsonFile(projectPath, 'Project config must contain valid JSON') ?? {};
|
|
489
|
+
if (typeof existing.language === 'string' && existing.language.trim().length > 0) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
validateProjectBootstrapConfigPathForWrite(projectRoot, projectPath);
|
|
493
|
+
writeFileSync(projectPath, JSON.stringify({ ...existing, language: inferredLanguage }, null, 2), 'utf-8');
|
|
494
|
+
}
|
|
375
495
|
export function readConfig(projectRoot) {
|
|
376
496
|
const detectedRoot = projectRoot ?? findProjectRoot(process.cwd());
|
|
377
497
|
const userPath = getUserConfigPath();
|
|
378
498
|
const projectPath = getProjectConfigPath(detectedRoot);
|
|
379
499
|
const userConfig = toPeaksConfig(readJsonFile(userPath));
|
|
380
|
-
const projectConfig =
|
|
500
|
+
const projectConfig = removeProjectSensitiveConfig(toPeaksConfig(readJsonFile(projectPath)));
|
|
381
501
|
const { proxy: projectProxy, ...projectConfigWithoutProxy } = projectConfig;
|
|
382
502
|
return {
|
|
383
503
|
...DEFAULT_CONFIG,
|
|
@@ -413,7 +533,7 @@ export function writeConfig(partial, layer = 'user') {
|
|
|
413
533
|
export function getConfig(options = {}) {
|
|
414
534
|
const projectRoot = findProjectRoot(process.cwd());
|
|
415
535
|
const userConfig = readJsonFile(getUserConfigPath()) ?? {};
|
|
416
|
-
const projectConfig =
|
|
536
|
+
const projectConfig = removeProjectSensitiveConfig(readJsonFile(getProjectConfigPath(projectRoot)) ?? {});
|
|
417
537
|
const { proxy: projectProxy, ...projectConfigWithoutProxy } = projectConfig;
|
|
418
538
|
const source = options.layer === 'user' ? userConfig : options.layer === 'project' ? projectConfig : { ...userConfig, ...projectConfigWithoutProxy };
|
|
419
539
|
const config = isRecord(source) ? { ...source, ...(source.tokens !== undefined ? { tokens: toTokenConfig(source.tokens) } : {}) } : source;
|
|
@@ -465,6 +585,9 @@ function readLayerConfig(layer) {
|
|
|
465
585
|
: { currentWorkspace: null, workspaces: [] };
|
|
466
586
|
}
|
|
467
587
|
export function addWorkspace(workspace, layer = 'user') {
|
|
588
|
+
if (!isSafeConfigSegment(workspace.workspaceId)) {
|
|
589
|
+
throw new Error('Workspace id must only contain letters, numbers, dots, underscores, or hyphens and must not contain path traversal');
|
|
590
|
+
}
|
|
468
591
|
const config = readLayerConfig(layer);
|
|
469
592
|
const workspaces = config.workspaces;
|
|
470
593
|
const existing = workspaces.findIndex((w) => w.workspaceId === workspace.workspaceId);
|
|
@@ -474,6 +597,8 @@ export function addWorkspace(workspace, layer = 'user') {
|
|
|
474
597
|
writeConfig({ workspaces: updatedWorkspaces }, layer);
|
|
475
598
|
}
|
|
476
599
|
export function removeWorkspace(workspaceId, layer = 'user') {
|
|
600
|
+
if (!isSafeConfigSegment(workspaceId))
|
|
601
|
+
return false;
|
|
477
602
|
const config = readLayerConfig(layer);
|
|
478
603
|
const workspaces = config.workspaces;
|
|
479
604
|
const idx = workspaces.findIndex((w) => w.workspaceId === workspaceId);
|
|
@@ -485,6 +610,8 @@ export function removeWorkspace(workspaceId, layer = 'user') {
|
|
|
485
610
|
return true;
|
|
486
611
|
}
|
|
487
612
|
export function setCurrentWorkspace(workspaceId, layer = 'user') {
|
|
613
|
+
if (!isSafeConfigSegment(workspaceId))
|
|
614
|
+
return false;
|
|
488
615
|
const config = readLayerConfig(layer);
|
|
489
616
|
const workspaces = config.workspaces;
|
|
490
617
|
const exists = workspaces.some((w) => w.workspaceId === workspaceId);
|
|
@@ -27,15 +27,26 @@ export type ModelProviderConfig = {
|
|
|
27
27
|
export type ProxyConfig = {
|
|
28
28
|
httpProxy?: string;
|
|
29
29
|
};
|
|
30
|
+
export type ArtifactProvider = 'github' | 'gitlab';
|
|
31
|
+
export type ArtifactRemoteRepoConfig = {
|
|
32
|
+
provider: ArtifactProvider;
|
|
33
|
+
owner: string;
|
|
34
|
+
name: string;
|
|
35
|
+
};
|
|
36
|
+
export type ArtifactStorageConfig = {
|
|
37
|
+
mode: 'local';
|
|
38
|
+
localPath?: string;
|
|
39
|
+
} | {
|
|
40
|
+
mode: 'local-with-remote-sync';
|
|
41
|
+
localPath?: string;
|
|
42
|
+
remote: ArtifactRemoteRepoConfig;
|
|
43
|
+
};
|
|
30
44
|
export type WorkspaceConfig = {
|
|
31
45
|
workspaceId: string;
|
|
32
46
|
name: string;
|
|
33
47
|
rootPath: string;
|
|
34
|
-
artifactRepo?:
|
|
35
|
-
|
|
36
|
-
owner: string;
|
|
37
|
-
name: string;
|
|
38
|
-
};
|
|
48
|
+
artifactRepo?: ArtifactRemoteRepoConfig;
|
|
49
|
+
artifactStorage?: ArtifactStorageConfig;
|
|
39
50
|
installedCapabilityIds: string[];
|
|
40
51
|
};
|
|
41
52
|
export type PeaksConfig = {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { basename, relative, resolve } from 'node:path';
|
|
4
|
+
import { isInsidePath } from '../../shared/path-utils.js';
|
|
4
5
|
import { getCurrentWorkspaceConfig } from '../config/config-service.js';
|
|
5
|
-
import { getArtifactWorkspaceStatus, getLocalArtifactPath } from '../artifacts/workspace-service.js';
|
|
6
|
+
import { getArtifactRemoteRepo, getArtifactWorkspaceStatus, getLocalArtifactPath } from '../artifacts/workspace-service.js';
|
|
6
7
|
const REQUIRED_ARTIFACTS = [
|
|
7
8
|
{ name: 'artifact-retention-report.md', path: ['qa', 'artifact-retention-report.md'] },
|
|
8
9
|
{ name: 'change-impact.json', path: ['sc', 'change-impact.json'] },
|
|
@@ -63,8 +64,8 @@ function mapSyncState(syncStatus) {
|
|
|
63
64
|
return 'pending';
|
|
64
65
|
return 'failed';
|
|
65
66
|
}
|
|
66
|
-
function getCurrentArtifactDir(
|
|
67
|
-
const peaksPath = getPeaksPath(
|
|
67
|
+
function getCurrentArtifactDir(artifactWorkspacePath) {
|
|
68
|
+
const peaksPath = getPeaksPath(artifactWorkspacePath);
|
|
68
69
|
const changeId = resolveCurrentChangeId(peaksPath);
|
|
69
70
|
const effectiveChangeId = changeId ?? 'unknown-change';
|
|
70
71
|
return {
|
|
@@ -73,14 +74,30 @@ function getCurrentArtifactDir(workspaceRoot) {
|
|
|
73
74
|
changeDir: resolve(peaksPath, 'changes', effectiveChangeId)
|
|
74
75
|
};
|
|
75
76
|
}
|
|
76
|
-
function getRetentionChangeDir(
|
|
77
|
-
const peaksPath = getPeaksPath(
|
|
77
|
+
function getRetentionChangeDir(artifactWorkspacePath, sliceId) {
|
|
78
|
+
const peaksPath = getPeaksPath(artifactWorkspacePath);
|
|
78
79
|
return {
|
|
79
80
|
peaksPath,
|
|
80
81
|
changeId: sliceId,
|
|
81
82
|
changeDir: resolve(peaksPath, 'changes', sliceId)
|
|
82
83
|
};
|
|
83
84
|
}
|
|
85
|
+
function isRetainedArtifactFile(filePath, artifactWorkspacePath, changesRoot, changeDir) {
|
|
86
|
+
if (!existsSync(filePath))
|
|
87
|
+
return false;
|
|
88
|
+
try {
|
|
89
|
+
const artifactWorkspaceRealPath = realpathSync(artifactWorkspacePath);
|
|
90
|
+
const changesRootRealPath = realpathSync(changesRoot);
|
|
91
|
+
const changeDirRealPath = realpathSync(changeDir);
|
|
92
|
+
const fileRealPath = realpathSync(filePath);
|
|
93
|
+
return isInsidePath(changesRootRealPath, artifactWorkspaceRealPath)
|
|
94
|
+
&& isInsidePath(changeDirRealPath, changesRootRealPath)
|
|
95
|
+
&& isInsidePath(fileRealPath, changeDirRealPath);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
84
101
|
export function getChangeTraceabilityStatus() {
|
|
85
102
|
const workspace = getCurrentWorkspaceConfig();
|
|
86
103
|
const artifactStatus = getArtifactWorkspaceStatus(workspace?.workspaceId);
|
|
@@ -98,8 +115,10 @@ export function getChangeTraceabilityStatus() {
|
|
|
98
115
|
nextActions: ['Add a workspace: peaks config workspace add --id <id> --name <name> --path <path>']
|
|
99
116
|
};
|
|
100
117
|
}
|
|
101
|
-
const
|
|
102
|
-
const
|
|
118
|
+
const artifactWorkspacePath = getLocalArtifactPath(workspace);
|
|
119
|
+
const { peaksPath, changeId, changeDir } = getCurrentArtifactDir(artifactWorkspacePath);
|
|
120
|
+
const artifactRepo = getArtifactRemoteRepo(workspace);
|
|
121
|
+
const hasArtifactRepo = Boolean(artifactRepo);
|
|
103
122
|
const requiredArtifacts = REQUIRED_ARTIFACTS.map((artifact) => {
|
|
104
123
|
const artifactPath = resolve(changeDir, ...artifact.path);
|
|
105
124
|
return {
|
|
@@ -112,11 +131,7 @@ export function getChangeTraceabilityStatus() {
|
|
|
112
131
|
if (!changeId) {
|
|
113
132
|
nextActions.push('Set the current change in .peaks/current-change');
|
|
114
133
|
}
|
|
115
|
-
if (
|
|
116
|
-
nextActions.push('Configure artifact repo: peaks config workspace add --id <id> --provider github --repo-owner <owner> --repo-name <name>');
|
|
117
|
-
nextActions.push('Then run: peaks artifacts init --provider github --name <repo> --dry-run');
|
|
118
|
-
}
|
|
119
|
-
else if (artifactStatus.syncStatus === 'pending') {
|
|
134
|
+
if (hasArtifactRepo && artifactStatus.syncStatus === 'pending') {
|
|
120
135
|
nextActions.push(`Run peaks artifacts sync --workspace ${workspace.workspaceId} --dry-run`);
|
|
121
136
|
}
|
|
122
137
|
return {
|
|
@@ -130,7 +145,7 @@ export function getChangeTraceabilityStatus() {
|
|
|
130
145
|
}
|
|
131
146
|
export function createChangeImpact(options) {
|
|
132
147
|
const workspace = getCurrentWorkspaceConfig();
|
|
133
|
-
const artifactRepo = workspace
|
|
148
|
+
const artifactRepo = workspace ? getArtifactRemoteRepo(workspace) : null;
|
|
134
149
|
return {
|
|
135
150
|
changeId: options.changeId,
|
|
136
151
|
sourceArtifacts: options.sourceArtifacts ?? [],
|
|
@@ -195,10 +210,12 @@ export function validateArtifactRetention(sliceId) {
|
|
|
195
210
|
warnings: ['Slice id must stay inside .peaks/changes and only contain letters, numbers, dots, underscores, or hyphens']
|
|
196
211
|
};
|
|
197
212
|
}
|
|
198
|
-
const
|
|
213
|
+
const artifactWorkspacePath = getLocalArtifactPath(workspace);
|
|
214
|
+
const { peaksPath, changeDir } = getRetentionChangeDir(artifactWorkspacePath, sliceId);
|
|
215
|
+
const changesRoot = resolve(peaksPath, 'changes');
|
|
199
216
|
const missingArtifacts = RETENTION_REQUIREMENTS
|
|
200
217
|
.map(([folder, file]) => resolve(changeDir, folder, file))
|
|
201
|
-
.filter((filePath) => !
|
|
218
|
+
.filter((filePath) => !isRetainedArtifactFile(filePath, artifactWorkspacePath, changesRoot, changeDir))
|
|
202
219
|
.map((filePath) => relative(changeDir, filePath).replace(/\\/g, '/'));
|
|
203
220
|
return {
|
|
204
221
|
valid: missingArtifacts.length === 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "1.0.
|
|
1
|
+
export declare const CLI_VERSION = "1.0.3";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.0.
|
|
1
|
+
export const CLI_VERSION = "1.0.3";
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Peaks Skill Swarm
|
|
3
|
+
description: Peaks 专用输出风格:仅在 peaks skills 工作流中用东北幽默强化角色编排、蜂群开发、成本模式和交付证据。
|
|
4
|
+
keep-coding-instructions: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
This output style is self-gated. Apply the sections below only when the current task explicitly invokes or continues a Peaks skill workflow, including `/peaks-*`, `skills/peaks-*`, Peaks PRD/RD/QA/UI/SC/TXT/Solo work, or edits to this repository's `skills/` directory. For unrelated tasks, preserve the default Claude Code behavior and keep responses concise.
|
|
8
|
+
|
|
9
|
+
## Peaks response contract
|
|
10
|
+
|
|
11
|
+
When active, make the skill transition visually obvious with a light Northeastern Chinese humor tone. Keep technical facts, risks, commands, and evidence precise; use humor only in short labels or one-liners, never to obscure blockers or failures. Start the first response for a Peaks skill task with this banner:
|
|
12
|
+
|
|
13
|
+
```markdown
|
|
14
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
15
|
+
Peaks Skill Active: <skill-name> — 整活开工,但不整虚的
|
|
16
|
+
Role Chain: <PRD → RD → QA → SC, or single role>
|
|
17
|
+
Mode: <Solo | Assisted | Swarm | Strict | Economy>
|
|
18
|
+
Current Gate: <confirmation | dry-run | coverage | QA | commit boundary | handoff>
|
|
19
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Use visible layout elements, not just a different tone: heavy separators, bracketed badges, a three-step workflow strip, and compact evidence tables. Then include a short process preview before doing work:
|
|
23
|
+
|
|
24
|
+
```markdown
|
|
25
|
+
[流程条] ① <current role action> → ② <next gate or validation> → ③ <handoff / artifact / follow-up>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For swarm or economy mode, add a compact worker table when useful:
|
|
29
|
+
|
|
30
|
+
```markdown
|
|
31
|
+
| Worker | Scope | Model/Cost lane | Output | Stop condition |
|
|
32
|
+
| --- | --- | --- | --- | --- |
|
|
33
|
+
| RD-1 | <subsystem> | <high/economy/configured provider> | <artifact> | <done signal> |
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
For final evidence, prefer this visual block:
|
|
37
|
+
|
|
38
|
+
```markdown
|
|
39
|
+
┌─ Evidence ───────────────────────
|
|
40
|
+
│ Commands: <only commands that matter>
|
|
41
|
+
│ Artifacts: <paths or none>
|
|
42
|
+
│ Changed: <files or none>
|
|
43
|
+
│ Blocker: <blocker or none>
|
|
44
|
+
│ Next: <one next action>
|
|
45
|
+
└──────────────────────────────────
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For continuing turns in the same Peaks workflow, use a compact status header instead of the full banner:
|
|
49
|
+
|
|
50
|
+
```markdown
|
|
51
|
+
Peaks Skill: <skill-name> | Gate: <current gate> | Next: <one short action>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Structure active Peaks responses around:
|
|
55
|
+
|
|
56
|
+
1. **Role** — name the active Peaks role or role chain, for example PRD → RD → QA → SC.
|
|
57
|
+
2. **Mode** — state whether the workflow is Solo, Assisted, Swarm, Strict, or Economy.
|
|
58
|
+
3. **Gate** — show the current required gate: product confirmation, RD dry-run, coverage, QA acceptance, commit boundary, or handoff.
|
|
59
|
+
4. **Action** — describe the immediate next action in one short sentence before tool use.
|
|
60
|
+
5. **Evidence** — end with only the evidence that matters: commands, artifacts, changed files, blockers, and next action.
|
|
61
|
+
|
|
62
|
+
Do not produce long narrative logs. Prefer compact capsules, tables, and checklists when they reduce ambiguity. For unrelated non-Peaks tasks, do not show the banner.
|
|
63
|
+
|
|
64
|
+
## GStack alignment
|
|
65
|
+
|
|
66
|
+
Use gstack as a workflow reference for `Think → Plan → Build → Review → Test → Ship → Reflect`, but keep Peaks as the authority:
|
|
67
|
+
|
|
68
|
+
- Think maps to Peaks PRD and TXT context.
|
|
69
|
+
- Plan maps to Peaks RD/UI planning, risk matrices, and slice contracts.
|
|
70
|
+
- Build maps to RD implementation under strict specs.
|
|
71
|
+
- Review maps to code review, design review, and security review.
|
|
72
|
+
- Test maps to QA regression and acceptance evidence.
|
|
73
|
+
- Ship maps to SC commit boundaries, sync state, and rollback points.
|
|
74
|
+
- Reflect maps to TXT lessons and reusable memory candidates.
|
|
75
|
+
|
|
76
|
+
Do not imply that gstack commands are available unless the project has explicitly installed or exposed them.
|
|
77
|
+
|
|
78
|
+
## Swarm development mode
|
|
79
|
+
|
|
80
|
+
Use Swarm mode for broad, parallelizable work with separable responsibilities. When recommending or running swarm work:
|
|
81
|
+
|
|
82
|
+
- split workers by role, risk, or subsystem;
|
|
83
|
+
- give each worker a bounded brief, expected artifact, and stop condition;
|
|
84
|
+
- require a reducer pass that merges findings, removes conflicts, and chooses the smallest safe implementation;
|
|
85
|
+
- keep shared-state actions, commits, pushes, deploys, and external messages behind explicit confirmation;
|
|
86
|
+
- report worker outputs as a compact matrix: worker, scope, result, blocker, next action.
|
|
87
|
+
|
|
88
|
+
Prefer parallel agents only for independent work. Do not duplicate searches or reviews already assigned to a worker.
|
|
89
|
+
|
|
90
|
+
## Economy mode
|
|
91
|
+
|
|
92
|
+
Use Economy mode when the user asks for low-cost execution or when the task is broad but low-risk. In Economy mode:
|
|
93
|
+
|
|
94
|
+
- reserve high-capability models for architecture, reducer decisions, security-sensitive work, and final review;
|
|
95
|
+
- route routine summarization, first-pass classification, repetitive inspection, and draft generation to cheaper available workers or providers when the environment supports them;
|
|
96
|
+
- treat MiniMax and similar low-cost models as candidate worker backends only when the current toolchain exposes them or the user authorizes that routing;
|
|
97
|
+
- never claim MiniMax or another external model was used unless an actual configured tool or agent invocation used it;
|
|
98
|
+
- escalate from Economy to Strict when the task touches security, destructive operations, data loss risk, releases, or unclear requirements.
|
|
99
|
+
|
|
100
|
+
When explaining Economy mode, separate **available now** from **recommended if configured**.
|
|
101
|
+
|
|
102
|
+
## Peaks RD code-output rule
|
|
103
|
+
|
|
104
|
+
When the active role is Peaks RD and code is produced or modified, require repeated dry-runs:
|
|
105
|
+
|
|
106
|
+
1. run applicable Peaks standards dry-runs before planning or implementation;
|
|
107
|
+
2. rerun relevant dry-runs after each meaningful slice or standards-affecting decision;
|
|
108
|
+
3. rerun before handoff, review, or commit-boundary work;
|
|
109
|
+
4. include dry-run command, result, and remaining action in the RD handoff capsule.
|
|
110
|
+
|
|
111
|
+
If a dry-run cannot be executed, state the blocker and keep it as the next action rather than silently skipping it.
|
|
112
|
+
|
|
113
|
+
## Output examples
|
|
114
|
+
|
|
115
|
+
### Active Peaks skill
|
|
116
|
+
|
|
117
|
+
```markdown
|
|
118
|
+
Role: RD → QA
|
|
119
|
+
Mode: Swarm
|
|
120
|
+
Gate: RD dry-run before implementation
|
|
121
|
+
Action: I will run standards dry-runs, then split workers by subsystem.
|
|
122
|
+
|
|
123
|
+
Evidence:
|
|
124
|
+
- Commands: ...
|
|
125
|
+
- Artifacts: ...
|
|
126
|
+
- Blocker: none
|
|
127
|
+
- Next: reducer review
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Non-Peaks task
|
|
131
|
+
|
|
132
|
+
Use normal concise Claude Code responses without the Peaks role/mode/gate wrapper.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Peaks CLI and short skill family for Claude Code automation.",
|
|
5
5
|
"author": "SquabbyZ",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"scripts/install-skills.mjs",
|
|
23
23
|
"scripts/watch.mjs",
|
|
24
24
|
"skills/**",
|
|
25
|
+
"output-styles/**",
|
|
25
26
|
"schemas/*.json"
|
|
26
27
|
],
|
|
27
28
|
"scripts": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, readdirSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, readdirSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
@@ -29,13 +29,22 @@ function markManagedPeaksLink(targetPath, sourcePath) {
|
|
|
29
29
|
writeFileSync(markerPath, `${sourcePath}\n`, 'utf8');
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function isManagedPeaksOutputStyle(managedTarget, outputStyleName) {
|
|
33
|
+
if (managedTarget === null) return false;
|
|
34
|
+
return managedTarget.replaceAll('\\', '/').endsWith(`/output-styles/${outputStyleName}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createInstallResult() {
|
|
38
|
+
return { installed: [], skipped: [] };
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
export function installBundledSkills(options = {}) {
|
|
33
42
|
const packageRoot = resolve(options.packageRoot ?? join(dirname(fileURLToPath(import.meta.url)), '..'));
|
|
34
43
|
const skillsRoot = join(packageRoot, 'skills');
|
|
35
44
|
const targetRoot = resolve(options.targetRoot ?? process.env.PEAKS_CLAUDE_SKILLS_DIR ?? join(homedir(), '.claude', 'skills'));
|
|
36
45
|
|
|
37
46
|
if (process.env.PEAKS_SKIP_SKILL_INSTALL === '1' || !existsSync(skillsRoot)) {
|
|
38
|
-
return
|
|
47
|
+
return createInstallResult();
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
const installed = [];
|
|
@@ -75,18 +84,66 @@ export function installBundledSkills(options = {}) {
|
|
|
75
84
|
return { installed, skipped };
|
|
76
85
|
}
|
|
77
86
|
|
|
87
|
+
export function installBundledOutputStyles(options = {}) {
|
|
88
|
+
const packageRoot = resolve(options.packageRoot ?? join(dirname(fileURLToPath(import.meta.url)), '..'));
|
|
89
|
+
const outputStylesRoot = join(packageRoot, 'output-styles');
|
|
90
|
+
const targetRoot = resolve(options.targetRoot ?? process.env.PEAKS_CLAUDE_OUTPUT_STYLES_DIR ?? join(homedir(), '.claude', 'output-styles'));
|
|
91
|
+
|
|
92
|
+
if (process.env.PEAKS_SKIP_SKILL_INSTALL === '1' || !existsSync(outputStylesRoot)) {
|
|
93
|
+
return createInstallResult();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const installed = [];
|
|
97
|
+
const skipped = [];
|
|
98
|
+
mkdirSync(targetRoot, { recursive: true });
|
|
99
|
+
|
|
100
|
+
for (const outputStyleName of readdirSync(outputStylesRoot)) {
|
|
101
|
+
const sourcePath = join(outputStylesRoot, outputStyleName);
|
|
102
|
+
const targetPath = join(targetRoot, outputStyleName);
|
|
103
|
+
|
|
104
|
+
if (!lstatSync(sourcePath).isFile() || !outputStyleName.endsWith('.md')) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const current = getPathStats(targetPath);
|
|
109
|
+
if (current) {
|
|
110
|
+
const managedTarget = getManagedTarget(targetPath);
|
|
111
|
+
if (isManagedPeaksOutputStyle(managedTarget, outputStyleName)) {
|
|
112
|
+
unlinkSync(targetPath);
|
|
113
|
+
unlinkSync(`${targetPath}.peaks-managed`);
|
|
114
|
+
} else {
|
|
115
|
+
skipped.push(outputStyleName);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
copyFileSync(sourcePath, targetPath);
|
|
121
|
+
markManagedPeaksLink(targetPath, sourcePath);
|
|
122
|
+
installed.push(outputStyleName);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { installed, skipped };
|
|
126
|
+
}
|
|
127
|
+
|
|
78
128
|
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
79
129
|
try {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
130
|
+
const skillsResult = installBundledSkills();
|
|
131
|
+
const outputStylesResult = installBundledOutputStyles();
|
|
132
|
+
if (skillsResult.installed.length > 0) {
|
|
133
|
+
process.stdout.write(`Peaks skills linked: ${skillsResult.installed.join(', ')}\n`);
|
|
134
|
+
}
|
|
135
|
+
if (skillsResult.skipped.length > 0) {
|
|
136
|
+
process.stderr.write(`Peaks skills skipped because local files already exist: ${skillsResult.skipped.join(', ')}\n`);
|
|
137
|
+
}
|
|
138
|
+
if (outputStylesResult.installed.length > 0) {
|
|
139
|
+
process.stdout.write(`Peaks output styles installed: ${outputStylesResult.installed.join(', ')}\n`);
|
|
83
140
|
}
|
|
84
|
-
if (
|
|
85
|
-
process.stderr.write(`Peaks
|
|
141
|
+
if (outputStylesResult.skipped.length > 0) {
|
|
142
|
+
process.stderr.write(`Peaks output styles skipped because local files already exist: ${outputStylesResult.skipped.join(', ')}\n`);
|
|
86
143
|
}
|
|
87
144
|
} catch (error) {
|
|
88
145
|
const message = error instanceof Error ? error.message : String(error);
|
|
89
|
-
process.stderr.write(`Peaks skills were not
|
|
146
|
+
process.stderr.write(`Peaks skills and output styles were not installed: ${message}\n`);
|
|
90
147
|
process.exitCode = 1;
|
|
91
148
|
}
|
|
92
149
|
}
|
|
@@ -26,6 +26,14 @@ For refactor workflows, avoid writing a full product PRD unless needed. Produce
|
|
|
26
26
|
- risk notes;
|
|
27
27
|
- user confirmation record.
|
|
28
28
|
|
|
29
|
+
## GStack integration
|
|
30
|
+
|
|
31
|
+
Use gstack as a concrete workflow reference for the product-facing parts of `Think → Plan → Build → Review → Test → Ship → Reflect`:
|
|
32
|
+
|
|
33
|
+
- map `/office-hours`-style exploration to Peaks goal, non-goal, and design-doc artifacts;
|
|
34
|
+
- map CEO/product plan review to user-confirmable product assumptions and acceptance criteria;
|
|
35
|
+
- preserve Peaks artifact gates instead of copying gstack commands verbatim.
|
|
36
|
+
|
|
29
37
|
## External capability guidance
|
|
30
38
|
|
|
31
39
|
Use `peaks capabilities --source mcp-server --json` before recommending product or workflow methodology resources.
|
package/skills/peaks-qa/SKILL.md
CHANGED
|
@@ -29,6 +29,18 @@ If the repo needs a first-time standards bundle, treat `standards init` as the c
|
|
|
29
29
|
|
|
30
30
|
For refactors, QA must be involved before implementation. It defines the regression and acceptance surface, then verifies the same surface after implementation.
|
|
31
31
|
|
|
32
|
+
## GStack integration
|
|
33
|
+
|
|
34
|
+
Use gstack as a concrete QA workflow reference for the `Review → Test → Ship` stages:
|
|
35
|
+
|
|
36
|
+
- map `/qa` and `/qa-only` browser validation concepts to Peaks regression matrices and validation reports;
|
|
37
|
+
- map regression-test creation to Peaks acceptance checks and coverage evidence;
|
|
38
|
+
- keep Peaks QA as the acceptance authority, with gstack browser and QA patterns as references only when capabilities and user approval allow them.
|
|
39
|
+
|
|
40
|
+
## Compact handoff
|
|
41
|
+
|
|
42
|
+
Before QA work stops, finishes, blocks, or hands off, emit a short resumable capsule: validation surface, coverage status, commands run, pass/fail summary, artifact paths, residual risks, blockers, and next action. Link to logs, coverage reports, regression matrices, and validation reports instead of pasting full outputs.
|
|
43
|
+
|
|
32
44
|
## External capability guidance
|
|
33
45
|
|
|
34
46
|
Use `peaks capabilities --source access-repo --json` before recommending browser or validation MCPs.
|
package/skills/peaks-rd/SKILL.md
CHANGED
|
@@ -25,6 +25,22 @@ Before RD planning or implementation work in a code repository, call the Peaks C
|
|
|
25
25
|
|
|
26
26
|
If `CLAUDE.md` is missing, treat creation as the preferred path. If `CLAUDE.md` already exists, use `standards update` to decide whether to append a managed index block or surface review-only suggestions. Apply only when write authorization exists; otherwise keep the CLI output as a preflight next action. Do not hand-write standards file mutations inside the skill.
|
|
27
27
|
|
|
28
|
+
## GStack integration and code dry-runs
|
|
29
|
+
|
|
30
|
+
Use gstack as a concrete engineering workflow reference for `Think → Plan → Build → Review → Test → Ship → Reflect`:
|
|
31
|
+
|
|
32
|
+
- map plan engineering review to Peaks RD risk matrices, task graphs, and slice contracts;
|
|
33
|
+
- map build/review discipline to strict spec-first implementation and code-review gates;
|
|
34
|
+
- map investigate/careful/guard concepts to root-cause analysis, risky-action confirmation, and scoped edit boundaries;
|
|
35
|
+
- adapt gstack concepts into Peaks artifacts rather than invoking gstack commands as runtime dependencies.
|
|
36
|
+
|
|
37
|
+
When Peaks RD produces or changes code, dry-run repeatedly instead of only during preflight:
|
|
38
|
+
|
|
39
|
+
1. run standards dry-runs before planning or implementation;
|
|
40
|
+
2. run the relevant Peaks dry-run again after each meaningful implementation slice or standards-affecting decision;
|
|
41
|
+
3. run the relevant dry-run before handoff, review, or commit-boundary work;
|
|
42
|
+
4. record dry-run command, result, and remaining action in the RD handoff capsule.
|
|
43
|
+
|
|
28
44
|
## Refactor hard gates
|
|
29
45
|
|
|
30
46
|
If a request is refactor, cleanup, architecture adjustment, module split, or technical debt work:
|
|
@@ -39,6 +55,38 @@ If a request is refactor, cleanup, architecture adjustment, module split, or tec
|
|
|
39
55
|
8. require 100% acceptance for the slice;
|
|
40
56
|
9. require code and intermediate artifacts to be committed before continuing.
|
|
41
57
|
|
|
58
|
+
## OpenSpec usage
|
|
59
|
+
|
|
60
|
+
For non-trivial RD changes, use OpenSpec when the project already has `openspec/` or the user approves adding OpenSpec. Create or update `openspec/changes/<change-id>/proposal.md`, `design.md`, `tasks.md`, and `specs/**/spec.md` before implementation slices begin.
|
|
61
|
+
|
|
62
|
+
OpenSpec artifacts are durable project specification files, not Peaks runtime swarm artifacts. They may live in the target repository root under `openspec/changes/...`. Swarm/runtime outputs such as task graphs, worker briefs, worker reports, reducer reports, scan reports, validation evidence, and compact handoffs must remain in the configured Peaks artifact workspace outside the target repository.
|
|
63
|
+
|
|
64
|
+
Peaks PRD/RD/QA gates remain authoritative: OpenSpec structures the durable spec, while Peaks artifacts still carry role handoffs, coverage gates, QA evidence, swarm coordination, and execution state.
|
|
65
|
+
|
|
66
|
+
## Frontend project generation
|
|
67
|
+
|
|
68
|
+
When RD work creates a frontend application and the user has not specified a technology stack, and the current scan plus existing project standards still do not establish a frontend stack, default to React + Vite + shadcn/ui with:
|
|
69
|
+
|
|
70
|
+
- `pnpm dlx shadcn@latest init --preset [CODE] --template vite`
|
|
71
|
+
|
|
72
|
+
`[CODE]` is the preset code supplied by the shadcn registry or user workflow; if it is unknown, stop and resolve the intended preset before scaffolding.
|
|
73
|
+
|
|
74
|
+
If the user specifies a frontend stack or scaffold command, use the specified technology. If the scaffold emits JavaScript, convert generated application files to TypeScript before continuing; if conversion is not practical, ask for a TypeScript-compatible scaffold.
|
|
75
|
+
|
|
76
|
+
Application projects generated through this skill must not contain JavaScript source or config files. Generate TypeScript only (`.ts`, `.tsx`, and TypeScript config equivalents), including when adapting examples from libraries or templates.
|
|
77
|
+
|
|
78
|
+
## Artifact and standards output
|
|
79
|
+
|
|
80
|
+
When project identification or scanning produces reports, matrices, maps, plans, or validation files, write them under the configured Peaks artifact workspace outside the target repository, not the repository root. If the artifact workspace is unknown, stop and resolve it before writing generated outputs. Use one session directory inside that workspace consistently so generated outputs stay grouped.
|
|
81
|
+
|
|
82
|
+
When project-local `CLAUDE.md` or project-local `.claude/rules/**` is created or updated, route the mutation through `peaks standards init` or `peaks standards update`; do not hand-write standards mutations. Derive the content from the current scan results and existing project standards. Keep only the rules that match the project's languages, frameworks, tooling, and repository layout. Do not emit generic templates, copy-pasted boilerplate, or rules unrelated to the current scan evidence. Do not update user-global `~/.claude/rules/**` from this workflow.
|
|
83
|
+
|
|
84
|
+
If the scan results are insufficient to justify a rule, leave it out or surface a review-only suggestion instead of writing it into project standards.
|
|
85
|
+
|
|
86
|
+
## Compact handoff
|
|
87
|
+
|
|
88
|
+
Before RD work stops, finishes, blocks, or hands off to another role, emit a short resumable capsule: mode, scope, coverage status, validated decisions, current slice, artifact paths, blockers, and next action. Link to scan reports, matrices, plans, and task graphs instead of restating them.
|
|
89
|
+
|
|
42
90
|
## External capability guidance
|
|
43
91
|
|
|
44
92
|
Use `peaks capabilities --source access-repo --json` and `peaks capabilities --source mcp-server --json` as the source of truth before recommending external resources.
|
|
@@ -46,7 +94,7 @@ Use `peaks capabilities --source access-repo --json` and `peaks capabilities --s
|
|
|
46
94
|
- Context7 can support current library/API documentation lookup when the map says it is available or the user authorizes MCP access.
|
|
47
95
|
- SearchCode can support external code discovery only after confirming the query will not expose secrets or private code.
|
|
48
96
|
- everything-claude-code, Claude Code Best Practice, mattpocock/skills, and andrej-karpathy-skills are RD guidance or review references; apply project-local conventions first.
|
|
49
|
-
- OpenSpec
|
|
97
|
+
- OpenSpec should structure durable spec-first RD changes when available or approved, but Peaks PRD/RD/QA gates remain authoritative.
|
|
50
98
|
- GitNexus remains a future proxied repository-intelligence boundary; do not install or run it directly.
|
|
51
99
|
|
|
52
100
|
## Boundaries
|
package/skills/peaks-sc/SKILL.md
CHANGED
|
@@ -19,6 +19,14 @@ Peaks SC records how product, RD, QA, code, and artifacts move together.
|
|
|
19
19
|
|
|
20
20
|
Each refactor slice must leave a traceable commit boundary containing code changes and PRD/RD/QA/TXT intermediate artifacts.
|
|
21
21
|
|
|
22
|
+
## GStack integration
|
|
23
|
+
|
|
24
|
+
Use gstack as a concrete source-control and release workflow reference for the `Ship → Reflect` stages:
|
|
25
|
+
|
|
26
|
+
- map `/ship` and `/land-and-deploy` concepts to Peaks commit boundaries, sync state, rollback points, and artifact retention;
|
|
27
|
+
- map checkpoint discipline to traceable code-plus-artifact slices;
|
|
28
|
+
- do not create PRs, merge, deploy, or mutate shared state unless the active Peaks workflow and user confirmation explicitly allow it.
|
|
29
|
+
|
|
22
30
|
## Project memory backup
|
|
23
31
|
|
|
24
32
|
Project `.claude/memory` is the primary source for durable project memory. At approved checkpoints, use `peaks memory sync --project <path> --workspace <artifact-workspace> --apply` to back up the full project memory directory into the artifact repository workspace; do not treat the artifact backup as a second writable memory source.
|
|
@@ -31,6 +31,28 @@ Peaks Solo must not silently:
|
|
|
31
31
|
|
|
32
32
|
Use the Peaks CLI for runtime side effects.
|
|
33
33
|
|
|
34
|
+
## GStack integration
|
|
35
|
+
|
|
36
|
+
Use gstack as a concrete orchestration reference for the full `Think → Plan → Build → Review → Test → Ship → Reflect` loop:
|
|
37
|
+
|
|
38
|
+
- map gstack role reviews to Peaks PRD, RD, UI, QA, SC, and TXT artifacts;
|
|
39
|
+
- map `/autoplan`-style review pipelines to Peaks mode selection and role handoffs;
|
|
40
|
+
- map `/retro` to Peaks TXT final context and reusable lessons;
|
|
41
|
+
- preserve Peaks confirmation gates, artifact workspace boundaries, and role separation instead of delegating orchestration to gstack commands.
|
|
42
|
+
|
|
43
|
+
## Mode selection
|
|
44
|
+
|
|
45
|
+
When the user invokes Peaks Solo without explicitly selecting an execution profile, use `AskUserQuestion` before orchestration starts. Present the recommended full-auto path as the first/default option, and give every option a practical description so users can choose quickly.
|
|
46
|
+
|
|
47
|
+
Offer these profiles unless the active command narrows the valid set:
|
|
48
|
+
|
|
49
|
+
1. **Full auto (Recommended, Solo profile)** — Peaks handles planning, role coordination, validation, and compact handoff end-to-end while preserving required confirmation gates for risky or shared-state actions.
|
|
50
|
+
2. **Assisted** — Peaks proposes plans, artifacts, and checks, then pauses for user decisions at major workflow boundaries.
|
|
51
|
+
3. **Swarm** — Peaks maximizes safe parallel role/worker execution for larger RD or QA workloads while keeping reducer validation and artifact boundaries explicit.
|
|
52
|
+
4. **Strict** — Peaks uses the most conservative gates: explicit confirmations, strict slice specs, coverage evidence, QA acceptance, and commit boundaries before continuing.
|
|
53
|
+
|
|
54
|
+
If the user already names a profile, do not ask again unless the request crosses a risk boundary or the named profile conflicts with required Peaks gates.
|
|
55
|
+
|
|
34
56
|
## Project standards preflight
|
|
35
57
|
|
|
36
58
|
Before orchestrating an end-to-end code repository workflow, gather the project standards preflight status from RD and QA by calling the Peaks CLI:
|
|
@@ -56,6 +78,12 @@ It must enforce the shared refactor red lines:
|
|
|
56
78
|
6. require 100% acceptance for each slice;
|
|
57
79
|
7. require code and intermediate artifacts to be committed before the next slice.
|
|
58
80
|
|
|
81
|
+
## Completion handoff
|
|
82
|
+
|
|
83
|
+
After a Peaks Solo workflow reaches final validation, refresh the project-local standards from the current scan-backed evidence before the handoff closes. Route project-local `CLAUDE.md` and project-local `.claude/rules/**` writes through `peaks standards init` or `peaks standards update`; do not hand-write standards mutations. If write authorization exists, apply an incremental merge of scan-backed changes into existing project-local standards. Preserve existing hand-maintained content unless the user explicitly confirms deletion or rewrite. If write authorization or the CLI path is unavailable, keep the standards output as the next action instead of writing it.
|
|
84
|
+
|
|
85
|
+
Use Peaks TXT for the final, blocked, or interrupted handoff capsule. Keep that capsule compact: current mode, validated decisions, artifact paths, standards deltas, open questions, and next action. Do not restate the full workflow log when a short handoff plus artifact links will do.
|
|
86
|
+
|
|
59
87
|
## Optional capabilities
|
|
60
88
|
|
|
61
89
|
When built-in guidance is insufficient, use capability discovery rather than reimplementing specialist workflows. Ask for user consent before token-heavy discovery unless the active profile permits it.
|
|
@@ -18,6 +18,18 @@ Peaks TXT compresses workflow context into portable, role-specific artifacts.
|
|
|
18
18
|
|
|
19
19
|
For refactors, create initial context before RD analysis and final context after validation and artifact retention.
|
|
20
20
|
|
|
21
|
+
## Compaction-safe outputs
|
|
22
|
+
|
|
23
|
+
When used alone or when a workflow needs portable artifacts that must survive session compaction, end with a short structured capsule: mode, validated decisions, artifact paths, standards deltas, open questions, and next action. Prefer links or paths over long narrative. Do not duplicate the full workflow log when a compact capsule is enough.
|
|
24
|
+
|
|
25
|
+
## GStack integration
|
|
26
|
+
|
|
27
|
+
Use gstack as a concrete context and reflection workflow reference for the `Reflect` stage:
|
|
28
|
+
|
|
29
|
+
- map `/retro` summaries to Peaks lessons, discarded options, and staleness conditions;
|
|
30
|
+
- map documentation-release ideas to compact downstream context for PRD, RD, QA, UI, and SC;
|
|
31
|
+
- keep durable memory writes behind Peaks memory extraction and user-approved persistence.
|
|
32
|
+
|
|
21
33
|
## Project memory guidance
|
|
22
34
|
|
|
23
35
|
When a skill artifact contains reusable project facts, decisions, rules, or constraints, mark only the stable extract with:
|
package/skills/peaks-ui/SKILL.md
CHANGED
|
@@ -19,6 +19,14 @@ Peaks UI handles experience, interaction, visual direction, and UI-specific refa
|
|
|
19
19
|
|
|
20
20
|
Only engage when the refactor affects UI, interaction, styling, page structure, design system, or frontend user behavior.
|
|
21
21
|
|
|
22
|
+
## GStack integration
|
|
23
|
+
|
|
24
|
+
Use gstack as a concrete design-review workflow reference for the `Plan → Review → Test` UI stages:
|
|
25
|
+
|
|
26
|
+
- map design review concepts to Peaks UX flow, page-state, interaction, and visual constraint artifacts;
|
|
27
|
+
- map browser walkthrough concepts to UI regression seeds when runtime validation is approved;
|
|
28
|
+
- keep accessibility, performance, and product-specific visual direction as Peaks UI acceptance inputs.
|
|
29
|
+
|
|
22
30
|
## External capability guidance
|
|
23
31
|
|
|
24
32
|
Use `peaks capabilities --json` before recommending design, browser, or UI reference resources.
|