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 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 workspace = { workspaceId: options.id, name: options.name, rootPath: options.path, installedCapabilityIds: [] };
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', 'en')).action((options) => {
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?.artifactRepo ?? null;
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' : 'configure') : 'configure',
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 artifactRepo configured'
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 { basename, dirname, resolve } from 'node:path';
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
- const rootPath = resolve(workspace.rootPath);
16
- return resolve(dirname(rootPath), `${basename(rootPath)}.peaks-artifacts`);
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 || !workspace.artifactRepo) {
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: 'No artifact repository configured for this workspace'
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 remoteUrl = getPublicRemoteUrl(workspace.artifactRepo);
105
- const gitAuthEnv = getGitAuthEnv(workspace.artifactRepo);
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 hasArtifactRepo = !!workspace.artifactRepo;
209
+ const artifactRepo = getArtifactRemoteRepo(workspace);
187
210
  const hasSafeBoundary = isArtifactWorkspaceOutsideTarget(workspace, localPath);
188
- const syncStatus = !hasArtifactRepo || !hasSafeBoundary
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: hasArtifactRepo && hasSafeBoundary,
219
+ configured: hasSafeBoundary,
197
220
  syncStatus,
198
221
  lastSync: null,
199
222
  hasLocalChanges: false,
200
- artifactRepo: workspace.artifactRepo ?? null,
223
+ artifactRepo,
201
224
  nextActions: !hasSafeBoundary
202
225
  ? ['Configure artifact workspace outside the target repository.']
203
- : hasArtifactRepo
226
+ : artifactRepo
204
227
  ? [`Run peaks artifacts sync --workspace ${workspace.workspaceId} --dry-run`]
205
- : [`Configure artifact repo: peaks config workspace add --id ${workspace.workspaceId} --provider github --repo-owner <owner> --repo-name <name>`]
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 || !workspace.artifactRepo) {
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 remoteUrl = workspace.artifactRepo.provider === 'github'
232
- ? `https://github.com/${workspace.artifactRepo.owner}/${workspace.artifactRepo.name}.git`
233
- : `https://gitlab.com/${workspace.artifactRepo.owner}/${workspace.artifactRepo.name}.git`;
234
- const plannedCommands = dryRun
235
- ? [
236
- `# Sync plan for workspace ${workspace.workspaceId}`,
237
- `# Local: ${localPath}`,
238
- `# Remote: ${remoteUrl}`,
239
- '# peaks artifacts sync --workspace ' + workspace.workspaceId,
240
- '# (dry-run only — no changes made)'
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
- `# Sync execution for workspace ${workspace.workspaceId}`,
244
- `# Confirm: will sync ${localPath} with ${remoteUrl}`,
245
- '# Exit 1 if not confirmed'
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 removeProjectProviderSecrets(config) {
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
- let artifactRepo;
225
- if (isRecord(value.artifactRepo) && (value.artifactRepo.provider === 'github' || value.artifactRepo.provider === 'gitlab') && typeof value.artifactRepo.owner === 'string' && typeof value.artifactRepo.name === 'string') {
226
- artifactRepo = { provider: value.artifactRepo.provider, owner: value.artifactRepo.owner, name: value.artifactRepo.name };
227
- }
228
- return artifactRepo ? { workspaceId, name, rootPath, installedCapabilityIds, artifactRepo } : { workspaceId, name, rootPath, installedCapabilityIds };
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 = removeProjectProviderSecrets(toPeaksConfig(readJsonFile(projectPath)));
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 = removeProjectProviderSecrets(readJsonFile(getProjectConfigPath(projectRoot)) ?? {});
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
- provider: 'github' | 'gitlab';
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(workspaceRoot) {
67
- const peaksPath = getPeaksPath(workspaceRoot);
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(workspaceRoot, sliceId) {
77
- const peaksPath = getPeaksPath(workspaceRoot);
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 { peaksPath, changeId, changeDir } = getCurrentArtifactDir(workspace.rootPath);
102
- const hasArtifactRepo = Boolean(workspace.artifactRepo);
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 (!hasArtifactRepo) {
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?.artifactRepo ?? null;
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 { changeDir } = getRetentionChangeDir(workspace.rootPath, sliceId);
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) => !existsSync(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.2";
1
+ export declare const CLI_VERSION = "1.0.3";
@@ -1 +1 @@
1
- export const CLI_VERSION = "1.0.2";
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.2",
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 { installed: [], skipped: [] };
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 result = installBundledSkills();
81
- if (result.installed.length > 0) {
82
- process.stdout.write(`Peaks skills linked: ${result.installed.join(', ')}\n`);
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 (result.skipped.length > 0) {
85
- process.stderr.write(`Peaks skills skipped because local files already exist: ${result.skipped.join(', ')}\n`);
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 linked: ${message}\n`);
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.
@@ -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.
@@ -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 can shape spec-first RD artifacts, but Peaks PRD/RD/QA gates remain authoritative.
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
@@ -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:
@@ -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.