peaks-cli 1.0.11 → 1.0.12

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.
@@ -1,4 +1,5 @@
1
- import { addWorkspace, getConfig, getMiniMaxProviderConfig, getMiniMaxProviderStatus, isSensitiveConfigPath, readConfig, redactConfigSecrets, removeWorkspace, setConfig, setCurrentWorkspace, setMiniMaxProviderConfig } from '../../services/config/config-service.js';
1
+ import { getLocalArtifactPath } from '../../services/artifacts/workspace-service.js';
2
+ import { addWorkspace, ensureWorkspaceConfigForPath, getConfig, getMiniMaxProviderConfig, getMiniMaxProviderStatus, isSensitiveConfigPath, readConfig, redactConfigSecrets, removeWorkspace, setConfig, setCurrentWorkspace, setMiniMaxProviderConfig } from '../../services/config/config-service.js';
2
3
  import { testMiniMaxProvider } from '../../services/providers/minimax-provider-service.js';
3
4
  import { fail, ok } from '../../shared/result.js';
4
5
  import { addJsonOption, getErrorMessage, isArtifactProvider, isArtifactRepoSegment, isMiniMaxHttpsUrl, parseConfigLayer, printInvalidConfigLayer, printResult, redactSensitiveErrorMessage, summarizeMiniMaxSmokeResult } from '../cli-helpers.js';
@@ -135,6 +136,21 @@ function registerWorkspaceCommands(config, io) {
135
136
  const cfg = readConfig();
136
137
  printResult(io, ok('config.workspace.list', { currentWorkspace: cfg.currentWorkspace, workspaces: cfg.workspaces }), options.json);
137
138
  });
139
+ addJsonOption(configWorkspace.command('ensure').description('Ensure a user workspace exists for a project path and make it current').option('--path <path>', 'project path to ensure, defaults to cwd')).action((options) => {
140
+ try {
141
+ const workspace = ensureWorkspaceConfigForPath(options.path ?? process.cwd());
142
+ if (!workspace) {
143
+ printResult(io, fail('config.workspace.ensure', 'WORKSPACE_ENSURE_FAILED', 'Could not resolve a workspace for the provided path', {}, ['Run from inside a project or pass --path <project>']), options.json);
144
+ process.exitCode = 1;
145
+ return;
146
+ }
147
+ printResult(io, ok('config.workspace.ensure', { workspace, currentWorkspace: workspace.workspaceId, artifactWorkspacePath: getLocalArtifactPath(workspace) }), options.json);
148
+ }
149
+ catch (error) {
150
+ printResult(io, fail('config.workspace.ensure', 'WORKSPACE_ENSURE_FAILED', getErrorMessage(error), {}, ['Check that the project path exists and artifact workspace markers are safe']), options.json);
151
+ process.exitCode = 1;
152
+ }
153
+ });
138
154
  addJsonOption(configWorkspace.command('add').description('Add a workspace').requiredOption('--id <id>', 'workspace identifier').requiredOption('--name <name>', 'workspace display name').requiredOption('--path <path>', 'workspace root path').option('--provider <provider>', 'artifact repo provider: github or gitlab').option('--repo-owner <owner>', 'artifact repo owner').option('--repo-name <name>', 'artifact repo name').option('--layer <layer>', 'user or project')).action((options) => {
139
155
  const layer = parseConfigLayer(options.layer);
140
156
  if (layer === null) {
@@ -11,21 +11,20 @@ import { getLocalArtifactPath } from '../../services/artifacts/workspace-service
11
11
  import { fail, ok } from '../../shared/result.js';
12
12
  import { addJsonOption, failUnsupportedNonDryRun, getErrorMessage, isRecommendationWorkflow, printResult } from '../cli-helpers.js';
13
13
  function getCurrentWorkspaceContext() {
14
- const workspace = getCurrentWorkspaceConfig();
14
+ const workspace = ensureWorkspaceConfigForCurrentPath() ?? getCurrentWorkspaceConfig();
15
15
  if (!workspace)
16
16
  return {};
17
17
  return { workspace, artifactWorkspacePath: getLocalArtifactPath(workspace) };
18
18
  }
19
19
  function getWorkflowWorkspaceContext() {
20
- try {
21
- const workspace = ensureWorkspaceConfigForCurrentPath() ?? getCurrentWorkspaceConfig();
22
- if (!workspace)
23
- return {};
24
- return { workspace, artifactWorkspacePath: getLocalArtifactPath(workspace) };
25
- }
26
- catch {
20
+ const workspace = ensureWorkspaceConfigForCurrentPath() ?? getCurrentWorkspaceConfig();
21
+ if (!workspace)
27
22
  return {};
28
- }
23
+ return { workspace, artifactWorkspacePath: getLocalArtifactPath(workspace) };
24
+ }
25
+ function failWorkflowWorkspaceContext(io, command, error, asJson) {
26
+ printResult(io, fail(command, 'WORKSPACE_CONTEXT_FAILED', getErrorMessage(error), {}, ['Run peaks config workspace ensure --path <project> --json and resolve the reported workspace issue']), asJson);
27
+ process.exitCode = 1;
29
28
  }
30
29
  function parseMaxWorkers(io, command, value, asJson) {
31
30
  const maxWorkers = Number(value);
@@ -65,24 +64,39 @@ function runTechPlan(io, options) {
65
64
  }
66
65
  try {
67
66
  validatePlanningInput(options.changeId, options.goal);
68
- const workspaceContext = getCurrentWorkspaceContext();
69
- const plan = createTechPlan({
70
- changeId: options.changeId,
71
- goal: options.goal,
72
- swarm: options.swarm ?? false,
73
- dryRun: true,
74
- ...workspaceContext
75
- });
76
- printResult(io, ok('tech.plan', plan), options.json);
77
67
  }
78
68
  catch (error) {
79
69
  printResult(io, fail('tech.plan', 'INVALID_CHANGE_ID_OR_GOAL', getErrorMessage(error), {}, ['Use a safe change id and a non-empty goal']), options.json);
80
70
  process.exitCode = 1;
71
+ return;
72
+ }
73
+ let workspaceContext;
74
+ try {
75
+ workspaceContext = getCurrentWorkspaceContext();
76
+ }
77
+ catch (error) {
78
+ failWorkflowWorkspaceContext(io, 'tech.plan', error, options.json);
79
+ return;
81
80
  }
81
+ const plan = createTechPlan({
82
+ changeId: options.changeId,
83
+ goal: options.goal,
84
+ swarm: options.swarm ?? false,
85
+ dryRun: true,
86
+ ...workspaceContext
87
+ });
88
+ printResult(io, ok('tech.plan', plan), options.json);
82
89
  }
83
90
  function runTechStatus(io, options) {
91
+ let workspaceContext;
92
+ try {
93
+ workspaceContext = getCurrentWorkspaceContext();
94
+ }
95
+ catch (error) {
96
+ failWorkflowWorkspaceContext(io, 'tech.status', error, options.json);
97
+ return;
98
+ }
84
99
  try {
85
- const workspaceContext = getCurrentWorkspaceContext();
86
100
  printResult(io, ok('tech.status', getTechStatus({ changeId: options.changeId, ...workspaceContext })), options.json);
87
101
  }
88
102
  catch (error) {
@@ -108,23 +122,31 @@ function runWorkflowRoute(io, options) {
108
122
  return;
109
123
  try {
110
124
  validatePlanningInput(options.changeId, options.goal);
111
- const workspaceContext = getWorkflowWorkspaceContext();
112
- const plan = createWorkflowRouterPlan({
113
- changeId: options.changeId,
114
- goal: options.goal,
115
- mode: options.mode,
116
- ...(soloMode ? { soloMode } : {}),
117
- maxWorkers,
118
- dryRun: true,
119
- config: readConfig(),
120
- ...workspaceContext
121
- });
122
- printResult(io, ok('workflow.route', plan), options.json);
123
125
  }
124
126
  catch (error) {
125
127
  printResult(io, fail('workflow.route', 'INVALID_CHANGE_ID_OR_GOAL', getErrorMessage(error), {}, ['Use a safe change id and a non-empty goal']), options.json);
126
128
  process.exitCode = 1;
129
+ return;
127
130
  }
131
+ let workspaceContext;
132
+ try {
133
+ workspaceContext = getWorkflowWorkspaceContext();
134
+ }
135
+ catch (error) {
136
+ failWorkflowWorkspaceContext(io, 'workflow.route', error, options.json);
137
+ return;
138
+ }
139
+ const plan = createWorkflowRouterPlan({
140
+ changeId: options.changeId,
141
+ goal: options.goal,
142
+ mode: options.mode,
143
+ ...(soloMode ? { soloMode } : {}),
144
+ maxWorkers,
145
+ dryRun: true,
146
+ config: readConfig(),
147
+ ...workspaceContext
148
+ });
149
+ printResult(io, ok('workflow.route', plan), options.json);
128
150
  }
129
151
  function runAutonomousWorkflow(io, options) {
130
152
  if (options.dryRun === false) {
@@ -144,23 +166,31 @@ function runAutonomousWorkflow(io, options) {
144
166
  return;
145
167
  try {
146
168
  validatePlanningInput(options.changeId, options.goal);
147
- const workspaceContext = getWorkflowWorkspaceContext();
148
- const plan = createAutonomousWorkflowPlan({
149
- changeId: options.changeId,
150
- goal: options.goal,
151
- mode: options.mode,
152
- ...(soloMode ? { soloMode } : {}),
153
- maxWorkers,
154
- dryRun: true,
155
- config: readConfig(),
156
- ...workspaceContext
157
- });
158
- printResult(io, ok('workflow.autonomous', plan), options.json);
159
169
  }
160
170
  catch (error) {
161
171
  printResult(io, fail('workflow.autonomous', 'INVALID_CHANGE_ID_OR_GOAL', getErrorMessage(error), {}, ['Use a safe change id and a non-empty goal']), options.json);
162
172
  process.exitCode = 1;
173
+ return;
174
+ }
175
+ let workspaceContext;
176
+ try {
177
+ workspaceContext = getWorkflowWorkspaceContext();
163
178
  }
179
+ catch (error) {
180
+ failWorkflowWorkspaceContext(io, 'workflow.autonomous', error, options.json);
181
+ return;
182
+ }
183
+ const plan = createAutonomousWorkflowPlan({
184
+ changeId: options.changeId,
185
+ goal: options.goal,
186
+ mode: options.mode,
187
+ ...(soloMode ? { soloMode } : {}),
188
+ maxWorkers,
189
+ dryRun: true,
190
+ config: readConfig(),
191
+ ...workspaceContext
192
+ });
193
+ printResult(io, ok('workflow.autonomous', plan), options.json);
164
194
  }
165
195
  function runSwarmPlan(io, options) {
166
196
  if ((options.skill ?? 'rd') !== 'rd') {
@@ -177,24 +207,32 @@ function runSwarmPlan(io, options) {
177
207
  return;
178
208
  try {
179
209
  validatePlanningInput(options.changeId, options.goal);
180
- const workspaceContext = getWorkflowWorkspaceContext();
181
- const config = readConfig();
182
- const plan = createRdSwarmPlan({
183
- skill: 'rd',
184
- changeId: options.changeId,
185
- goal: options.goal,
186
- maxWorkers,
187
- dryRun: true,
188
- swarmMode: config.swarmMode,
189
- executionModelId: getEconomyAwareExecutionModelId(config),
190
- ...workspaceContext
191
- });
192
- printResult(io, ok('swarm.plan', plan), options.json);
193
210
  }
194
211
  catch (error) {
195
212
  printResult(io, fail('swarm.plan', 'INVALID_CHANGE_ID_OR_GOAL', getErrorMessage(error), {}, ['Use a safe change id and a non-empty goal']), options.json);
196
213
  process.exitCode = 1;
214
+ return;
215
+ }
216
+ let workspaceContext;
217
+ try {
218
+ workspaceContext = getWorkflowWorkspaceContext();
197
219
  }
220
+ catch (error) {
221
+ failWorkflowWorkspaceContext(io, 'swarm.plan', error, options.json);
222
+ return;
223
+ }
224
+ const config = readConfig();
225
+ const plan = createRdSwarmPlan({
226
+ skill: 'rd',
227
+ changeId: options.changeId,
228
+ goal: options.goal,
229
+ maxWorkers,
230
+ dryRun: true,
231
+ swarmMode: config.swarmMode,
232
+ executionModelId: getEconomyAwareExecutionModelId(config),
233
+ ...workspaceContext
234
+ });
235
+ printResult(io, ok('swarm.plan', plan), options.json);
198
236
  }
199
237
  function addTechPlanOptions(command) {
200
238
  return addJsonOption(command
@@ -1,19 +1,15 @@
1
1
  import { existsSync, realpathSync, statSync } from 'node:fs';
2
2
  import { spawn } from 'node:child_process';
3
3
  import { createRequire } from 'node:module';
4
- import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
+ import { dirname, isAbsolute, relative, resolve, sep, win32 } from 'node:path';
5
5
  const CODEGRAPH_PACKAGE_NAME = '@colbymchenry/codegraph';
6
6
  const CODEGRAPH_PACKAGE_VERSION = '0.7.10';
7
7
  const CODEGRAPH_EXECUTABLE = process.execPath;
8
8
  const CODEGRAPH_BINARY_PATH = resolveCodegraphBinaryPath();
9
9
  const CODEGRAPH_PROCESS_TIMEOUT_MS = 600_000;
10
10
  const CODEGRAPH_OUTPUT_LIMIT_BYTES = 10 * 1024 * 1024;
11
- const NODE_OPTIONS_ENV_KEY = 'NODE_OPTIONS';
12
- const NPM_CONFIG_PREFIX = 'npm_config_';
13
- const NPM_CONFIG_UPPER_PREFIX = 'NPM_CONFIG_';
14
11
  const POSITIONAL_ARGUMENT_PREFIX = '-';
15
12
  const ALLOWED_SUBCOMMANDS = ['status', 'init', 'index', 'query', 'files', 'context', 'affected'];
16
- const NUMERIC_FLAG_NAMES = ['limit', 'maxDepth'];
17
13
  const COMMON_OPTION_KEYS = ['subcommand', 'project'];
18
14
  const ALLOWED_OPTIONS_BY_SUBCOMMAND = {
19
15
  status: [],
@@ -24,13 +20,16 @@ const ALLOWED_OPTIONS_BY_SUBCOMMAND = {
24
20
  context: ['task'],
25
21
  affected: ['files', 'json']
26
22
  };
23
+ function assertCodegraphBinaryExists(binaryPath) {
24
+ if (!existsSync(binaryPath)) {
25
+ throw new Error('Unable to resolve local codegraph binary from @colbymchenry/codegraph');
26
+ }
27
+ }
27
28
  function resolveCodegraphBinaryPath() {
28
29
  const require = createRequire(import.meta.url);
29
30
  const packageJsonPath = require.resolve('@colbymchenry/codegraph/package.json');
30
31
  const binaryPath = resolve(dirname(packageJsonPath), 'dist', 'bin', 'codegraph.js');
31
- if (!existsSync(binaryPath)) {
32
- throw new Error('Unable to resolve local codegraph binary from @colbymchenry/codegraph');
33
- }
32
+ assertCodegraphBinaryExists(binaryPath);
34
33
  return binaryPath;
35
34
  }
36
35
  function assertSupportedSubcommand(subcommand) {
@@ -38,12 +37,15 @@ function assertSupportedSubcommand(subcommand) {
38
37
  throw new Error(`Unsupported codegraph subcommand: ${subcommand}`);
39
38
  }
40
39
  }
40
+ function assertProjectRootDirectory(projectRoot) {
41
+ if (!statSync(projectRoot).isDirectory()) {
42
+ throw new Error('Project path must exist and be a directory');
43
+ }
44
+ }
41
45
  function resolveProjectRoot(project) {
42
46
  const projectRoot = resolve(project);
43
47
  try {
44
- if (!statSync(projectRoot).isDirectory()) {
45
- throw new Error('Project path must exist and be a directory');
46
- }
48
+ assertProjectRootDirectory(projectRoot);
47
49
  return realpathSync.native(projectRoot);
48
50
  }
49
51
  catch {
@@ -88,12 +90,8 @@ function assertInsideProject(projectRoot, absolutePath) {
88
90
  throw new Error('Affected files must stay inside the project');
89
91
  }
90
92
  }
91
- function resolveExistingBoundary(absoluteFilePath) {
92
- if (existsSync(absoluteFilePath)) {
93
- return absoluteFilePath;
94
- }
95
- let currentPath = dirname(absoluteFilePath);
96
- while (!existsSync(currentPath)) {
93
+ function climbToExistingBoundary(currentPath, pathExists = existsSync) {
94
+ while (!pathExists(currentPath)) {
97
95
  const parentPath = dirname(currentPath);
98
96
  if (parentPath === currentPath) {
99
97
  return currentPath;
@@ -102,6 +100,9 @@ function resolveExistingBoundary(absoluteFilePath) {
102
100
  }
103
101
  return currentPath;
104
102
  }
103
+ function resolveExistingBoundary(absoluteFilePath) {
104
+ return existsSync(absoluteFilePath) ? absoluteFilePath : climbToExistingBoundary(dirname(absoluteFilePath));
105
+ }
105
106
  function normalizeProjectRelativeFile(projectRoot, file) {
106
107
  assertPositionalArgument(file, 'Affected files');
107
108
  const absoluteFilePath = resolve(projectRoot, file);
@@ -110,10 +111,13 @@ function normalizeProjectRelativeFile(projectRoot, file) {
110
111
  assertInsideProject(projectRoot, realBoundary);
111
112
  return relative(projectRoot, absoluteFilePath).split(sep).join('/');
112
113
  }
113
- function buildAffectedFileArgs(projectRoot, files) {
114
+ function assertAffectedFiles(files) {
114
115
  if (!files || files.length < 1) {
115
116
  throw new Error('affected requires at least one file');
116
117
  }
118
+ }
119
+ function buildAffectedFileArgs(projectRoot, files) {
120
+ assertAffectedFiles(files);
117
121
  return files.map((file) => normalizeProjectRelativeFile(projectRoot, file));
118
122
  }
119
123
  function buildCommandArgs(options, projectRoot) {
@@ -168,24 +172,36 @@ function assertOutputLimit(currentSize, chunkSize) {
168
172
  }
169
173
  return nextSize;
170
174
  }
171
- function terminateCodegraphProcess(childProcess) {
175
+ function getWindowsTaskkillPath(fileExists = existsSync) {
176
+ const candidates = [
177
+ win32.join('C:\\Windows', 'System32', 'taskkill.exe'),
178
+ win32.join('C:\\WINNT', 'System32', 'taskkill.exe')
179
+ ];
180
+ return candidates.find((candidate) => fileExists(candidate)) ?? null;
181
+ }
182
+ function terminateCodegraphProcess(childProcess, platform = process.platform, killProcess = process.kill, spawnProcess = spawn, taskkillPath = getWindowsTaskkillPath()) {
172
183
  if (childProcess.pid === undefined) {
173
184
  childProcess.kill();
174
185
  return;
175
186
  }
176
- if (process.platform === 'win32') {
177
- const taskkillPath = process.env.SystemRoot ? join(process.env.SystemRoot, 'System32', 'taskkill.exe') : 'taskkill.exe';
178
- spawn(taskkillPath, ['/pid', String(childProcess.pid), '/T', '/F'], { shell: false, stdio: 'ignore' });
187
+ if (platform === 'win32') {
188
+ if (!taskkillPath) {
189
+ childProcess.kill();
190
+ return;
191
+ }
192
+ const killerProcess = spawnProcess(taskkillPath, ['/pid', String(childProcess.pid), '/T', '/F'], { shell: false, stdio: 'ignore' });
193
+ killerProcess.on('error', () => childProcess.kill());
194
+ killerProcess.unref();
179
195
  return;
180
196
  }
181
197
  try {
182
- process.kill(-childProcess.pid, 'SIGTERM');
198
+ killProcess(-childProcess.pid, 'SIGTERM');
183
199
  }
184
200
  catch {
185
201
  childProcess.kill('SIGTERM');
186
202
  }
187
203
  }
188
- function defaultCodegraphProcessRunner(invocation) {
204
+ function runCodegraphProcess(invocation, timeoutMs = CODEGRAPH_PROCESS_TIMEOUT_MS) {
189
205
  return new Promise((resolveResult, reject) => {
190
206
  const childProcess = spawn(invocation.executable, invocation.args, {
191
207
  cwd: invocation.cwd,
@@ -195,8 +211,8 @@ function defaultCodegraphProcessRunner(invocation) {
195
211
  });
196
212
  const timeout = setTimeout(() => {
197
213
  terminateCodegraphProcess(childProcess);
198
- reject(new Error(`codegraph process timed out after ${CODEGRAPH_PROCESS_TIMEOUT_MS}ms`));
199
- }, CODEGRAPH_PROCESS_TIMEOUT_MS);
214
+ reject(new Error(`codegraph process timed out after ${timeoutMs}ms`));
215
+ }, timeoutMs);
200
216
  const stdoutChunks = [];
201
217
  const stderrChunks = [];
202
218
  let stdoutSize = 0;
@@ -235,6 +251,9 @@ function defaultCodegraphProcessRunner(invocation) {
235
251
  });
236
252
  });
237
253
  }
254
+ function defaultCodegraphProcessRunner(invocation) {
255
+ return runCodegraphProcess(invocation);
256
+ }
238
257
  export function createCodegraphInvocation(options) {
239
258
  assertSupportedSubcommand(options.subcommand);
240
259
  const projectRoot = resolveProjectRoot(options.project);
@@ -156,6 +156,25 @@ function validateUserConfigPathForWrite(configPath) {
156
156
  }
157
157
  }
158
158
  }
159
+ function getExistingBoundary(path) {
160
+ let currentPath = resolve(path);
161
+ while (!existsSync(currentPath)) {
162
+ const parentPath = dirname(currentPath);
163
+ if (parentPath === currentPath) {
164
+ return currentPath;
165
+ }
166
+ currentPath = parentPath;
167
+ }
168
+ return currentPath;
169
+ }
170
+ function validateArtifactWorkspaceRootBeforeCreate(artifactRoot, workspaceRoot) {
171
+ const workspaceRootReal = realpathSync(workspaceRoot);
172
+ const existingBoundary = getExistingBoundary(artifactRoot);
173
+ const existingBoundaryReal = realpathSync(existingBoundary);
174
+ if (isInsidePath(resolve(artifactRoot), workspaceRoot) || isInsidePath(existingBoundaryReal, workspaceRootReal)) {
175
+ throw new Error('Artifact workspace must stay outside the project root');
176
+ }
177
+ }
159
178
  function validateArtifactWorkspaceRoot(artifactRoot, workspaceRoot) {
160
179
  const artifactStats = lstatSync(artifactRoot);
161
180
  if (!artifactStats.isDirectory() || artifactStats.isSymbolicLink()) {
@@ -871,6 +890,7 @@ function ensureArtifactWorkspaceMarker(workspace) {
871
890
  const artifactRoot = getWorkspaceArtifactRoot(workspace);
872
891
  const peaksPath = resolve(artifactRoot, '.peaks');
873
892
  const markerPath = resolve(peaksPath, 'config.json');
893
+ validateArtifactWorkspaceRootBeforeCreate(artifactRoot, workspace.rootPath);
874
894
  ensureDir(artifactRoot);
875
895
  validateArtifactWorkspaceRoot(artifactRoot, workspace.rootPath);
876
896
  ensureDir(peaksPath);
@@ -887,7 +907,7 @@ export function ensureWorkspaceConfigForPath(path = process.cwd()) {
887
907
  const existingWorkspace = findWorkspaceForPath(config.workspaces, path);
888
908
  if (existingWorkspace) {
889
909
  ensureArtifactWorkspaceMarker(existingWorkspace);
890
- if (!config.currentWorkspace) {
910
+ if (config.currentWorkspace !== existingWorkspace.workspaceId) {
891
911
  writeConfig({ currentWorkspace: existingWorkspace.workspaceId }, 'user');
892
912
  }
893
913
  return existingWorkspace;
@@ -903,7 +923,7 @@ export function ensureWorkspaceConfigForPath(path = process.cwd()) {
903
923
  };
904
924
  ensureArtifactWorkspaceMarker(workspace);
905
925
  const updatedWorkspaces = [...config.workspaces, workspace];
906
- writeConfig({ workspaces: updatedWorkspaces, ...(!config.currentWorkspace ? { currentWorkspace: workspace.workspaceId } : {}) }, 'user');
926
+ writeConfig({ workspaces: updatedWorkspaces, currentWorkspace: workspace.workspaceId }, 'user');
907
927
  return workspace;
908
928
  }
909
929
  export function getWorkspaceConfigForCurrentPath() {
@@ -87,7 +87,7 @@ export const seedCapabilityItems = [
87
87
  capability('codegraph.semantic-query', 'codegraph', 'Codegraph Semantic Query', 'cli', 'project-analysis', ['engineer'], 'medium', 'peaks-rd-local-scan', 'Use local Grep/Glob and RD scanning when codegraph semantic query is unavailable.', 'Codegraph Semantic Query', 'Codegraph 语义查询', 'Queries local symbols and project relationships for RD planning evidence.', '查询本地符号和项目关系,为 RD 规划提供证据。'),
88
88
  capability('codegraph.impact-analysis', 'codegraph', 'Codegraph Impact Analysis', 'cli', 'impact-analysis', ['engineer', 'qa'], 'medium', 'peaks-rd-qa-impact-review', 'Use RD changed-file analysis and QA regression planning when codegraph affected output is unavailable.', 'Codegraph Impact Analysis', 'Codegraph 影响面分析', 'Analyzes likely impact for changed files so RD and QA can focus planning and regression scope.', '分析变更文件的可能影响面,帮助 RD 与 QA 聚焦规划和回归范围。'),
89
89
  capability('codegraph.context-pack', 'codegraph', 'Codegraph Context Pack', 'cli', 'context-pack', ['engineer', 'qa', 'product'], 'medium', 'peaks-txt-context-capsule', 'Use Peaks TXT context capsules and role-skill handoffs when codegraph context output is unavailable.', 'Codegraph Context Pack', 'Codegraph 上下文包', 'Builds task-specific local context that Solo, RD, and TXT can use as supporting evidence.', '生成任务相关的本地上下文,作为 Solo、RD 与 TXT 的辅助证据。'),
90
- capability('playwright-mcp.browser-validation', 'playwright-mcp', 'Playwright MCP Browser Validation', 'mcp', 'browser-validation', ['engineer', 'qa'], 'medium', 'manual-browser-test', 'Use local Playwright or manual browser verification.', 'Playwright Browser Validation', 'Playwright 浏览器验证', 'Validates UI flows through controlled browser automation.', '通过受控浏览器自动化验证 UI 流程。'),
90
+ capability('gstack-browse.headed-browser-validation', 'gstack-browse', 'gstack Browse Headed Browser Validation', 'cli', 'browser-validation', ['engineer', 'qa'], 'medium', 'blocked-browser-validation', 'Block browser validation until headed gstack/browse/dist/browse can open the actual launched app URL.', 'gstack Browse Validation', 'gstack 有头浏览器验证', 'Validates UI flows in a visible gstack browse session against the app URL advertised by the launched dev server.', '使用有头 gstack browse 会话验证由开发服务器实际输出 URL 对应的 UI 流程。'),
91
91
  capability('chrome-devtools-mcp.browser-debug', 'chrome-devtools-mcp', 'Chrome DevTools Browser Debug', 'mcp', 'browser-debug', ['engineer', 'qa'], 'medium', 'manual-devtools-inspection', 'Use browser screenshots, console logs, and network traces supplied by the user.', 'Chrome DevTools Debug', 'Chrome DevTools 调试', 'Inspects runtime UI, console, network, and performance behavior.', '检查运行时 UI、控制台、网络和性能行为。'),
92
92
  capability('figma-context-mcp.design-context', 'figma-context-mcp', 'Figma Design Context', 'mcp', 'design-context', ['designer', 'engineer'], 'medium', 'manual-design-input', 'Ask the user for screenshots, tokens, or exported design notes.', 'Figma Design Context', 'Figma 设计上下文', 'Reads design context for UI implementation planning.', '读取设计上下文以辅助 UI 实现规划。'),
93
93
  capability('searchcode-mcp.code-search', 'searchcode-mcp', 'SearchCode MCP', 'mcp', 'code-search', ['engineer'], 'medium', 'local-code-search', 'Use local Grep/Glob and ask before sending private code externally.', 'External Code Search', '外部代码搜索', 'Searches code examples or repositories outside the current workspace.', '搜索当前工作区外部的代码示例或仓库。'),
@@ -8,7 +8,7 @@ export const seedCapabilityLandingMappings = [
8
8
  mapping({ capabilityId: 'codegraph.context-pack', sourceId: 'codegraph', sourceGroup: 'access-repo', landingKind: 'skill', target: 'peaks-rd', skillName: 'peaks-rd', guidance: 'Dry-run reference only: peaks-rd may use peaks codegraph context --project <path> <task> to gather local evidence for RD analysis when execution is approved, without replacing standards dry-runs.' }),
9
9
  mapping({ capabilityId: 'codegraph.context-pack', sourceId: 'codegraph', sourceGroup: 'access-repo', landingKind: 'skill', target: 'peaks-solo', skillName: 'peaks-solo', guidance: 'Solo may attach local context packs or affected summaries before role handoff so RD, QA, and TXT share the same project evidence.' }),
10
10
  mapping({ capabilityId: 'codegraph.context-pack', sourceId: 'codegraph', sourceGroup: 'access-repo', landingKind: 'skill', target: 'peaks-txt', skillName: 'peaks-txt', guidance: 'TXT may summarize recorded codegraph context packs into handoffs while treating them as supporting evidence only.' }),
11
- mapping({ capabilityId: 'playwright-mcp.browser-validation', sourceId: 'playwright-mcp', sourceGroup: 'access-repo', landingKind: 'skill', target: 'peaks-qa', skillName: 'peaks-qa', guidance: 'Use for browser and E2E validation after user-approved app targets are available.' }),
11
+ mapping({ capabilityId: 'gstack-browse.headed-browser-validation', sourceId: 'gstack-browse', sourceGroup: 'access-repo', landingKind: 'skill', target: 'peaks-qa', skillName: 'peaks-qa', guidance: 'Launch the app, capture its actual advertised URL, and validate only with headed gstack/browse/dist/browse; block instead of falling back to Playwright MCP or default ports.' }),
12
12
  mapping({ capabilityId: 'chrome-devtools-mcp.browser-debug', sourceId: 'chrome-devtools-mcp', sourceGroup: 'access-repo', landingKind: 'skill', target: 'peaks-ui', skillName: 'peaks-ui', guidance: 'Use for runtime UI, console, network, and performance inspection.' }),
13
13
  mapping({ capabilityId: 'context-mode.context-management', sourceId: 'context-mode', sourceGroup: 'access-repo', landingKind: 'skill', target: 'peaks-txt', skillName: 'peaks-txt', guidance: 'Use only for explicit context management; durable memory requires user opt-in.' }),
14
14
  mapping({ capabilityId: 'modelcontextprotocol-servers.collection', sourceId: 'modelcontextprotocol-servers', sourceGroup: 'access-repo', landingKind: 'catalog', target: 'future peaks mcp catalog', guidance: 'Treat as an unscanned MCP collection; do not auto-install unknown servers.' }),
@@ -2,7 +2,7 @@ export const seedCapabilitySources = [
2
2
  { sourceId: 'ruflo-access-repo', sourceType: 'repo', sourceGroup: 'access-repo', title: 'Ruflo', url: 'https://github.com/ruvnet/ruflo', trustSignals: { notes: ['Workflow orchestration reference; do not execute or install from the capability map.'] }, discoveryStatus: 'unscanned', items: ['ruflo-access-repo.workflow-reference'] },
3
3
  { sourceId: 'context7', sourceType: 'repo', sourceGroup: 'access-repo', title: 'Context7', url: 'https://github.com/upstash/context7', trustSignals: { sourceReputation: 'commonly used docs lookup MCP capability' }, discoveryStatus: 'indexed', items: ['context7.docs-lookup'] },
4
4
  { sourceId: 'codegraph', sourceType: 'repo', sourceGroup: 'access-repo', title: 'codegraph', url: 'https://github.com/colbymchenry/codegraph', trustSignals: { notes: ['Use through peaks codegraph only; do not run upstream install flows from the capability map.', 'Local project indexing can create .codegraph artifacts; do not commit generated databases unless explicitly requested.'] }, discoveryStatus: 'indexed', items: ['codegraph.project-indexing', 'codegraph.semantic-query', 'codegraph.impact-analysis', 'codegraph.context-pack'] },
5
- { sourceId: 'playwright-mcp', sourceType: 'repo', sourceGroup: 'access-repo', title: 'Playwright MCP', url: 'https://github.com/microsoft/playwright-mcp', trustSignals: { sourceReputation: 'Microsoft browser automation MCP server' }, discoveryStatus: 'indexed', items: ['playwright-mcp.browser-validation'] },
5
+ { sourceId: 'gstack-browse', sourceType: 'repo', sourceGroup: 'access-repo', title: 'gstack browse', url: 'https://github.com/garrytan/gstack', trustSignals: { notes: ['Use headed gstack/browse/dist/browse only after launching the app and discovering its actual advertised URL. Do not substitute Playwright MCP.'] }, discoveryStatus: 'indexed', items: ['gstack-browse.headed-browser-validation'] },
6
6
  { sourceId: 'chrome-devtools-mcp', sourceType: 'website', sourceGroup: 'access-repo', title: 'Chrome DevTools MCP', url: 'https://www.pulsemcp.com/servers/chrome-devtools', trustSignals: { notes: ['Browser inspection and performance debugging capability.'] }, discoveryStatus: 'indexed', items: ['chrome-devtools-mcp.browser-debug'] },
7
7
  { sourceId: 'context-mode', sourceType: 'repo', sourceGroup: 'access-repo', title: 'Context Mode', url: 'https://github.com/mksglu/context-mode', trustSignals: { notes: ['Context and memory management reference.'] }, discoveryStatus: 'indexed', items: ['context-mode.context-management'] },
8
8
  { sourceId: 'modelcontextprotocol-servers', sourceType: 'mcp-collection', sourceGroup: 'access-repo', title: 'Model Context Protocol Servers', url: 'https://github.com/modelcontextprotocol/servers', trustSignals: { sourceReputation: 'official MCP server collection' }, discoveryStatus: 'unscanned', items: ['modelcontextprotocol-servers.collection'] },
@@ -18,10 +18,6 @@ export type ShadcnExecutionResult = {
18
18
  stderr: string;
19
19
  };
20
20
  export type ShadcnProcessRunner = (invocation: ShadcnInvocation) => Promise<ShadcnExecutionResult>;
21
- declare function createShadcnEnvironment(sourceEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
22
21
  export declare function createShadcnInvocation(options: ShadcnInvocationOptions): ShadcnInvocation;
23
22
  export declare function executeShadcnInvocation(invocation: ShadcnInvocation, runner?: ShadcnProcessRunner): Promise<ShadcnExecutionResult>;
24
- export declare const testInternals: {
25
- createShadcnEnvironment: typeof createShadcnEnvironment;
26
- };
27
23
  export {};
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { spawn } from 'node:child_process';
3
3
  import { createRequire } from 'node:module';
4
- import { resolve } from 'node:path';
4
+ import { win32 } from 'node:path';
5
5
  const SHADCN_PACKAGE_NAME = 'shadcn';
6
6
  const SHADCN_PACKAGE_VERSION = '4.7.0';
7
7
  const SHADCN_EXECUTABLE = process.execPath;
@@ -10,12 +10,15 @@ const SHADCN_PROCESS_TIMEOUT_MS = 600_000;
10
10
  const SHADCN_OUTPUT_LIMIT_BYTES = 10 * 1024 * 1024;
11
11
  const POSITIONAL_ARGUMENT_PREFIX = '-';
12
12
  const PRESERVED_ENV_KEYS = ['PATH', 'Path', 'HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'TEMP', 'TMP', 'SystemRoot', 'WINDIR'];
13
- function resolveShadcnBinaryPath() {
14
- const require = createRequire(import.meta.url);
15
- const binaryPath = require.resolve('shadcn');
13
+ function assertShadcnBinaryExists(binaryPath) {
16
14
  if (!existsSync(binaryPath)) {
17
15
  throw new Error('Unable to resolve local shadcn binary from shadcn');
18
16
  }
17
+ }
18
+ function resolveShadcnBinaryPath() {
19
+ const require = createRequire(import.meta.url);
20
+ const binaryPath = require.resolve('shadcn');
21
+ assertShadcnBinaryExists(binaryPath);
19
22
  return binaryPath;
20
23
  }
21
24
  function assertShadcnArgs(args) {
@@ -43,24 +46,36 @@ function assertOutputLimit(currentSize, chunkSize) {
43
46
  }
44
47
  return nextSize;
45
48
  }
46
- function terminateShadcnProcess(childProcess) {
49
+ function getWindowsTaskkillPath(fileExists = existsSync) {
50
+ const candidates = [
51
+ win32.join('C:\\Windows', 'System32', 'taskkill.exe'),
52
+ win32.join('C:\\WINNT', 'System32', 'taskkill.exe')
53
+ ];
54
+ return candidates.find((candidate) => fileExists(candidate)) ?? null;
55
+ }
56
+ function terminateShadcnProcess(childProcess, platform = process.platform, killProcess = process.kill, spawnProcess = spawn, taskkillPath = getWindowsTaskkillPath()) {
47
57
  if (childProcess.pid === undefined) {
48
58
  childProcess.kill();
49
59
  return;
50
60
  }
51
- if (process.platform === 'win32') {
52
- const taskkillPath = process.env.SystemRoot ? resolve(process.env.SystemRoot, 'System32', 'taskkill.exe') : 'taskkill.exe';
53
- spawn(taskkillPath, ['/pid', String(childProcess.pid), '/T', '/F'], { shell: false, stdio: 'ignore' });
61
+ if (platform === 'win32') {
62
+ if (!taskkillPath) {
63
+ childProcess.kill();
64
+ return;
65
+ }
66
+ const killerProcess = spawnProcess(taskkillPath, ['/pid', String(childProcess.pid), '/T', '/F'], { shell: false, stdio: 'ignore' });
67
+ killerProcess.on('error', () => childProcess.kill());
68
+ killerProcess.unref();
54
69
  return;
55
70
  }
56
71
  try {
57
- process.kill(-childProcess.pid, 'SIGTERM');
72
+ killProcess(-childProcess.pid, 'SIGTERM');
58
73
  }
59
74
  catch {
60
75
  childProcess.kill('SIGTERM');
61
76
  }
62
77
  }
63
- function defaultShadcnProcessRunner(invocation) {
78
+ function runShadcnProcess(invocation, timeoutMs = SHADCN_PROCESS_TIMEOUT_MS) {
64
79
  return new Promise((resolveResult, reject) => {
65
80
  const childProcess = spawn(invocation.executable, invocation.args, {
66
81
  cwd: invocation.cwd,
@@ -70,8 +85,8 @@ function defaultShadcnProcessRunner(invocation) {
70
85
  });
71
86
  const timeout = setTimeout(() => {
72
87
  terminateShadcnProcess(childProcess);
73
- reject(new Error(`shadcn process timed out after ${SHADCN_PROCESS_TIMEOUT_MS}ms`));
74
- }, SHADCN_PROCESS_TIMEOUT_MS);
88
+ reject(new Error(`shadcn process timed out after ${timeoutMs}ms`));
89
+ }, timeoutMs);
75
90
  const stdoutChunks = [];
76
91
  const stderrChunks = [];
77
92
  let stdoutSize = 0;
@@ -110,6 +125,9 @@ function defaultShadcnProcessRunner(invocation) {
110
125
  });
111
126
  });
112
127
  }
128
+ function defaultShadcnProcessRunner(invocation) {
129
+ return runShadcnProcess(invocation);
130
+ }
113
131
  export function createShadcnInvocation(options) {
114
132
  assertShadcnArgs(options.args);
115
133
  return {
@@ -123,6 +141,3 @@ export function createShadcnInvocation(options) {
123
141
  export async function executeShadcnInvocation(invocation, runner = defaultShadcnProcessRunner) {
124
142
  return runner(invocation);
125
143
  }
126
- export const testInternals = {
127
- createShadcnEnvironment
128
- };
@@ -1 +1 @@
1
- export declare const CLI_VERSION = "1.0.11";
1
+ export declare const CLI_VERSION = "1.0.12";
@@ -1 +1 @@
1
- export const CLI_VERSION = "1.0.11";
1
+ export const CLI_VERSION = "1.0.12";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-cli",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "Peaks CLI and short skill family for Claude Code automation.",
5
5
  "author": "SquabbyZ",
6
6
  "license": "MIT",
@@ -21,6 +21,7 @@
21
21
  "scripts/sync-version.mjs",
22
22
  "scripts/install-skills.mjs",
23
23
  "scripts/watch.mjs",
24
+ "scripts/strip-internal-exports.mjs",
24
25
  "skills/**",
25
26
  "!skills/**/test-prompts.json",
26
27
  "!skills/**/.DS_Store",
@@ -28,7 +29,7 @@
28
29
  "schemas/*.json"
29
30
  ],
30
31
  "scripts": {
31
- "build": "node ./scripts/sync-version.mjs && node ./scripts/clean-dist.mjs && tsc -p tsconfig.json",
32
+ "build": "node ./scripts/sync-version.mjs && node ./scripts/clean-dist.mjs && tsc -p tsconfig.json && node ./scripts/strip-internal-exports.mjs",
32
33
  "prepack": "npm run build",
33
34
  "postinstall": "node ./scripts/install-skills.mjs",
34
35
  "dev": "tsx src/cli/index.ts",
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
8
+ const distSrcRoot = join(packageRoot, 'dist', 'src');
9
+ const internalExportPattern = /\/\*\* @internal \*\/\r?\nexport const testInternals = \{[\s\S]*?\r?\n\};\r?\n?/g;
10
+
11
+ function getJavaScriptFiles(directory) {
12
+ return readdirSync(directory).flatMap((entry) => {
13
+ const path = join(directory, entry);
14
+ const stats = statSync(path);
15
+ if (stats.isDirectory()) {
16
+ return getJavaScriptFiles(path);
17
+ }
18
+ return stats.isFile() && path.endsWith('.js') ? [path] : [];
19
+ });
20
+ }
21
+
22
+ for (const filePath of getJavaScriptFiles(distSrcRoot)) {
23
+ const source = readFileSync(filePath, 'utf8');
24
+ if (!source.includes('testInternals')) {
25
+ continue;
26
+ }
27
+
28
+ const strippedSource = source.replace(internalExportPattern, '');
29
+ if (strippedSource === source || strippedSource.includes('testInternals')) {
30
+ throw new Error(`Failed to strip internal test export from ${filePath}`);
31
+ }
32
+ writeFileSync(filePath, strippedSource, 'utf8');
33
+ }
@@ -55,17 +55,17 @@ QA cannot pass a change until the report contains evidence for every applicable
55
55
 
56
56
  1. **Unit tests** — run the project test command or a focused test command that covers new/changed code. For legacy projects below the target coverage, require coverage for the new or changed code rather than failing on pre-existing uncovered code.
57
57
  2. **API validation** — when the change touches API contracts, data loading, request handling, auth, or integrations, exercise the relevant API path and record request/response evidence or a justified local substitute.
58
- 3. **Frontend browser validation** — when the repository has a frontend or the change affects UI, launch the app and use headed `gstack/browse/dist/browse` for real browser end-to-end validation. Verify the visible browser with `browse status`, screenshot evidence, or user confirmation. If login, CAPTCHA, SSO, or MFA appears, wait for the user to complete login and explicitly confirm completion before continuing. Capture sanitized route/actions, sanitized screenshots or observations, sanitized console/network failures, and acceptance result.
58
+ 3. **Frontend browser validation** — when the repository has a frontend or the change affects UI, launch the app with the project's actual dev/preview command, capture the actual advertised reachable URL from server output/config/user input, and use headed `gstack/browse/dist/browse` against that exact URL for real browser end-to-end validation. Never assume framework default ports such as 3000, 5173, 4200, 4321, or 8080. Verify the visible browser with `browse status`, screenshot evidence, or user confirmation. If login, CAPTCHA, SSO, or MFA appears, wait for the user to complete login and explicitly confirm completion before continuing. Capture sanitized actual URL origin/path, route/actions, sanitized screenshots or observations, sanitized console/network failures, and acceptance result.
59
59
  4. **Browser-error feedback loop** — if `gstack/browse/dist/browse` shows a page error, console exception, broken network request, hydration/render failure, or visible regression, return the work to RD/development with the exact evidence. Do not pass QA until the fixed build is retested in the browser.
60
60
  5. **Security check** — run security review for the changed surface and dependency/config changes. Record findings, fixes, and unresolved risks.
61
61
  6. **Performance check** — run the project’s available performance check, build-size check, Lighthouse-equivalent check, or browser performance inspection appropriate to the change. Record baseline/after numbers when available.
62
62
  7. **Validation report** — write or link a report containing scope, environment, commands, sanitized browser evidence, security/performance results, pass/fail summary, residual risks, and next action.
63
63
 
64
- If headed `gstack/browse/dist/browse` is unavailable, mark the gate blocked with the missing capability. Screenshots, logs, manual steps, or other tools must not substitute for the mandatory frontend browser gate. Do not silently downgrade frontend validation to API-only testing.
64
+ If headed `gstack/browse/dist/browse` is unavailable, unstable, closes unexpectedly, or cannot verify a visible browser, mark the gate blocked with the missing/unstable capability. Screenshots, logs, manual steps, Playwright MCP, or other tools must not substitute for the mandatory frontend browser gate. Do not silently downgrade frontend validation to API-only testing.
65
65
 
66
66
  ## Local intermediate artifacts
67
67
 
68
- QA reports, sanitized browser evidence, logs, matrices, and validation summaries should be written to `.peaks/<session-id>/qa/` by default, or to the Peaks CLI-provided local artifact workspace. Do not store login URLs, cookies, headers, tokens, storage state, browser traces, or screenshots/logs containing PII or SSO/MFA material. Do not default to git-backed storage or external artifact sync unless the user or active profile explicitly authorizes it.
68
+ QA reports, sanitized browser evidence, logs, matrices, and validation summaries should be written under the Peaks CLI-provided `artifactWorkspacePath` at `.peaks/<session-id>/qa/` by default. Do not store login URLs, cookies, headers, tokens, storage state, browser traces, or screenshots/logs containing PII or SSO/MFA material. Do not default to target-repo `.peaks` storage, git-backed storage, or external artifact sync unless the CLI returned that location or the user/active profile explicitly authorizes it.
69
69
 
70
70
  ## Compact handoff
71
71
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  This reference documents artifact-contracts.md for peaks-qa.
4
4
 
5
- Default local artifact path: `.peaks/<session-id>/qa/`.
5
+ Default runtime artifact path: `<artifactWorkspacePath>/.peaks/<session-id>/qa/`, where `artifactWorkspacePath` comes from `peaks config workspace ensure --path <project> --json`.
6
6
 
7
- QA artifacts should include regression matrices, API evidence, headed `gstack/browse/dist/browse` E2E evidence, sanitized console/network observations, sanitized screenshots or observations, security/performance checks, validation report, residual risks, and blocked/final handoff capsules. Do not retain login URLs, cookies, headers, tokens, storage state, browser traces, or screenshots/logs containing PII or SSO/MFA material. Keep artifacts local by default. Do not commit or sync them unless explicitly authorized.
7
+ QA artifacts should include regression matrices, API evidence, headed `gstack/browse/dist/browse` E2E evidence against the actual launched app URL, sanitized console/network observations, sanitized screenshots or observations, security/performance checks, validation report, residual risks, and blocked/final handoff capsules. Do not retain login URLs, cookies, headers, tokens, storage state, browser traces, or screenshots/logs containing PII or SSO/MFA material. Keep runtime artifacts in the configured Peaks artifact workspace by default. Do not commit or sync them unless explicitly authorized.
@@ -9,7 +9,7 @@ QA must be involved before refactor implementation.
9
9
  - baseline report;
10
10
  - acceptance checks;
11
11
  - API validation evidence when API behavior is in scope;
12
- - headed `gstack/browse/dist/browse` browser E2E evidence when a frontend exists or UI is in scope, with mandatory visible-browser confirmation;
12
+ - headed `gstack/browse/dist/browse` browser E2E evidence when a frontend exists or UI is in scope, using the actual URL advertised by the launched app rather than default framework ports, with mandatory visible-browser confirmation;
13
13
  - security check evidence;
14
14
  - performance check evidence;
15
15
  - validation report;
@@ -21,4 +21,4 @@ UT coverage below 95%, missing coverage, or unverifiable coverage blocks refacto
21
21
 
22
22
  ## Frontend failure rule
23
23
 
24
- If browser validation shows page errors, console exceptions, failed critical network requests, or visible regressions, QA returns the change to RD with evidence and reruns the browser path after the fix.
24
+ If browser validation shows page errors, console exceptions, failed critical network requests, or visible regressions, QA returns the change to RD with evidence and reruns the browser path after the fix. If headed gstack browse is unavailable, unstable, closes unexpectedly, or cannot confirm a visible browser, QA blocks instead of falling back to Playwright MCP, screenshots-only evidence, or default-port probing.
@@ -65,7 +65,7 @@ RD cannot mark a development slice complete until all of these are true:
65
65
  1. OpenSpec change artifacts exist and are linked for non-trivial work when the target repo already has `openspec/`, or the user has approved adding it;
66
66
  2. unit tests covering the new or changed behavior have been added or updated and run successfully;
67
67
  3. if the repository is legacy and total UT coverage is below the project target, do not block on historical coverage, but require coverage evidence for newly added or changed code;
68
- 4. for frontend or UI-affecting slices, RD self-test has launched the app and used headed `gstack/browse/dist/browse` for real browser end-to-end validation with visible-browser confirmation, sanitized route/actions, sanitized console/network observations, and acceptance result recorded; if login, CAPTCHA, SSO, or MFA appears, wait for the user to complete login and explicitly confirm completion before continuing;
68
+ 4. for frontend or UI-affecting slices, RD self-test has launched the app with the project's actual dev/preview command, captured the actual advertised reachable URL from server output/config/user input, and used headed `gstack/browse/dist/browse` against that exact URL for real browser end-to-end validation with visible-browser confirmation, sanitized route/actions, sanitized console/network observations, and acceptance result recorded; never assume framework default ports such as 3000, 5173, 4200, 4321, or 8080; if `gstack/browse/dist/browse` is unavailable, unstable, closes unexpectedly, or cannot verify a visible browser, mark RD self-test blocked instead of falling back to Playwright MCP, screenshots-only evidence, or default-port probing; if login, CAPTCHA, SSO, or MFA appears, wait for the user to complete login and explicitly confirm completion before continuing;
69
69
  5. code review has been performed with findings recorded and CRITICAL/HIGH issues fixed before progression; unresolved CRITICAL/HIGH findings only allow a blocked handoff;
70
70
  6. security review has been performed for the changed surface, with CRITICAL/HIGH issues fixed before progression and particular attention to user input, file system access, external calls, auth, secrets, and dependency changes;
71
71
  7. the post-check dry-run has passed and is linked in the handoff.
@@ -84,7 +84,7 @@ If a request is refactor, cleanup, architecture adjustment, module split, or tec
84
84
  6. call or consume peaks-prd and peaks-qa artifacts even in direct RD mode;
85
85
  7. require strict slice spec before each slice;
86
86
  8. require 100% acceptance for the slice;
87
- 9. require code changes and intermediate artifacts to be traceable in local `.peaks/<session-id>/` storage before continuing; commit or sync artifacts only when explicitly authorized.
87
+ 9. require code changes and intermediate artifacts to be traceable under the Peaks CLI-provided `artifactWorkspacePath` at `.peaks/<session-id>/` before continuing; commit or sync artifacts only when explicitly authorized.
88
88
 
89
89
  ## OpenSpec usage
90
90
 
@@ -110,7 +110,7 @@ Application projects generated through this skill must not contain JavaScript so
110
110
 
111
111
  ## Artifact and standards output
112
112
 
113
- When project identification or scanning produces reports, matrices, maps, plans, or validation files, write them under the configured Peaks artifact workspace. By default, use local non-git storage at `.peaks/<session-id>/rd/` in the target project or the Peaks CLI-provided local workspace. If the artifact workspace is unknown, create or request `.peaks/<session-id>/` before writing generated outputs. Use one session directory consistently so generated outputs stay grouped.
113
+ When project identification or scanning produces reports, matrices, maps, plans, or validation files, write them under the configured Peaks artifact workspace returned by `peaks config workspace ensure --path <project> --json`. By default, use local non-git storage at `<artifactWorkspacePath>/.peaks/<session-id>/rd/`. If the artifact workspace is unknown, run/request `peaks config workspace ensure` before writing generated outputs. Use one session directory consistently so generated outputs stay grouped.
114
114
 
115
115
  Do not default to a git-backed artifact repository, external artifact sync, or automatic commits for intermediate artifacts. Git inclusion or sync requires explicit user confirmation or an active profile that clearly authorizes it. Browser evidence must be sanitized before retention: do not store login URLs, cookies, headers, tokens, storage state, browser traces, or screenshots/logs containing PII or SSO/MFA material.
116
116
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  This reference documents artifact-contracts.md for peaks-rd.
4
4
 
5
- Default local artifact path: `.peaks/<session-id>/rd/`.
5
+ Default runtime artifact path: `<artifactWorkspacePath>/.peaks/<session-id>/rd/`, where `artifactWorkspacePath` comes from `peaks config workspace ensure --path <project> --json`.
6
6
 
7
- RD artifacts should include scan reports, OpenSpec path notes, slice specs, task graphs, coverage evidence, code-review and security-review reports, post-check dry-run output, and handoff capsules. Keep artifacts local by default. Do not commit or sync them unless explicitly authorized.
7
+ RD artifacts should include scan reports, OpenSpec path notes, slice specs, task graphs, coverage evidence, code-review and security-review reports, post-check dry-run output, and handoff capsules. Keep runtime artifacts in the configured Peaks artifact workspace by default. Do not mix target-repo `.peaks` with the CLI-returned artifact workspace, and do not commit or sync artifacts unless explicitly authorized.
@@ -19,7 +19,7 @@
19
19
  - Each implemented slice must pass unit tests, code review, and security review before RD dry-run.
20
20
  - The post-check dry-run runs after tests, CR, and security review, not before them.
21
21
  - Each slice must pass 100% acceptance.
22
- - Code changes and sanitized intermediate artifacts must be traceable in local `.peaks/<session-id>/` storage before the next slice; commit or sync sanitized artifacts only when explicitly authorized. Browser evidence must not retain login URLs, cookies, headers, tokens, storage state, browser traces, or screenshots/logs containing PII or SSO/MFA material.
22
+ - Code changes and sanitized intermediate artifacts must be traceable under the Peaks CLI-provided `artifactWorkspacePath` at `.peaks/<session-id>/` before the next slice; commit or sync sanitized artifacts only when explicitly authorized. Browser evidence must not retain login URLs, cookies, headers, tokens, storage state, browser traces, or screenshots/logs containing PII or SSO/MFA material.
23
23
 
24
24
  ## Required artifacts
25
25
 
@@ -36,4 +36,4 @@
36
36
  - `security-review-report.md`
37
37
  - `post-check-dry-run.md`
38
38
  - `validation-report.md`
39
- - `retention-boundary.md` documenting local `.peaks/<session-id>/` traceability, browser-evidence sanitization, and any explicitly authorized commit/sync requirement
39
+ - `retention-boundary.md` documenting `artifactWorkspacePath/.peaks/<session-id>/` traceability, browser-evidence sanitization, and any explicitly authorized commit/sync requirement
@@ -40,13 +40,15 @@ Use gstack as a concrete orchestration reference for the full `Think → Plan
40
40
  - map `/retro` to Peaks TXT final context and reusable lessons;
41
41
  - preserve Peaks confirmation gates, artifact workspace boundaries, and role separation instead of delegating orchestration to gstack commands.
42
42
 
43
- For frontend workflows, Peaks Solo must ensure RD self-test and QA validation use headed `gstack/browse/dist/browse` for real browser end-to-end validation. A visible browser opening is mandatory. If login, CAPTCHA, SSO, or MFA appears, wait for the user to complete login and explicitly confirm completion before continuing. If browser validation reports page, console, network, render, or visible UI errors, route the workflow back to RD for fixes before QA can pass.
43
+ For frontend workflows, Peaks Solo must ensure RD self-test and QA validation launch the app with the project's actual dev/preview command, capture the actual advertised reachable URL from server output/config/user input, and use headed `gstack/browse/dist/browse` against that exact URL for real browser end-to-end validation. Never assume framework default ports such as 3000, 5173, 4200, 4321, or 8080. A visible browser opening is mandatory. If `gstack/browse/dist/browse` is unavailable, unstable, closes unexpectedly, or cannot verify a visible browser, mark the browser gate blocked instead of falling back to Playwright MCP, screenshots-only evidence, or default-port probing. If login, CAPTCHA, SSO, or MFA appears, wait for the user to complete login and explicitly confirm completion before continuing. If browser validation reports page, console, network, render, or visible UI errors, route the workflow back to RD for fixes before QA can pass.
44
44
 
45
45
  Browser validation artifacts must be sanitized before retention: do not store login URLs, cookies, headers, tokens, storage state, browser traces, or screenshots/logs containing PII or SSO/MFA material in `.peaks` artifacts, and do not commit or sync sensitive browser evidence.
46
46
 
47
47
  ## Local intermediate artifact workspace
48
48
 
49
- Peaks Solo should establish or discover a local `.peaks/<session-id>/` workspace before role handoffs. Store PRD/RD/UI/QA/SC/TXT intermediate artifacts there by default, with role subdirectories such as `prd/`, `rd/`, `ui/`, `qa/`, `sc/`, and `txt/`.
49
+ Before role handoffs, Peaks Solo must run or require `peaks config workspace ensure --path <project> --json` for the target project. This command must ensure the user `~/.peaks/config.json` contains a workspace for the current project and switch `currentWorkspace` to that workspace. Treat the returned `artifactWorkspacePath` as the single runtime artifact root for the workflow.
50
+
51
+ Store PRD/RD/UI/QA/SC/TXT intermediate artifacts under `<artifactWorkspacePath>/.peaks/<session-id>/` by default, with role subdirectories such as `prd/`, `rd/`, `ui/`, `qa/`, `sc/`, and `txt/`. Do not mix target-repo `.peaks/<session-id>/` with the configured artifact workspace unless the CLI explicitly returns a project-local artifact root. Record workspace id, project root, artifactWorkspacePath, session id, and any OpenSpec paths in every role handoff so later stages read and write the same directories.
50
52
 
51
53
  Do not default to a git-backed local artifact repository, external artifact sync, or automatic commits for intermediate artifacts. Only include sanitized `.peaks` artifacts in git, sync them elsewhere, or create external artifact repositories after explicit user confirmation or an active profile that clearly authorizes it.
52
54
 
@@ -128,7 +130,7 @@ It must enforce the shared refactor red lines:
128
130
  4. split broad refactors into minimal functional slices;
129
131
  5. require strict verifiable specs before each slice;
130
132
  6. require 100% acceptance for each slice;
131
- 7. require code changes and sanitized intermediate artifacts to be traceable in local `.peaks/<session-id>/` storage before the next slice; commit or sync sanitized artifacts only when explicitly authorized.
133
+ 7. require code changes and sanitized intermediate artifacts to be traceable under the Peaks CLI-provided `artifactWorkspacePath` at `.peaks/<session-id>/` before the next slice; commit or sync sanitized artifacts only when explicitly authorized.
132
134
 
133
135
  ## Completion handoff
134
136
 
@@ -140,7 +142,7 @@ Use Peaks TXT for the final, blocked, or interrupted handoff capsule. Keep that
140
142
 
141
143
  Codegraph is an optional project-analysis enhancement for role handoff. Solo may coordinate `peaks codegraph context --project <path> "<task>"` or `peaks codegraph affected --project <path> <changed-files...> --json` before assigning work to RD, QA, or TXT when shared project evidence would make the handoff narrower.
142
144
 
143
- Record useful output in the local Peaks artifact workspace, such as `.peaks/<session-id>/rd/codegraph-context.md` or `.peaks/<session-id>/rd/codegraph-affected.json`. Treat codegraph output as untrusted supporting evidence. Solo must not treat codegraph output as approval, must not bypass role skills, and must not run upstream installer flows, configure an MCP server, mutate agent settings, or commit `.codegraph/` artifacts.
145
+ Record useful output in the local Peaks artifact workspace, such as `<artifactWorkspacePath>/.peaks/<session-id>/rd/codegraph-context.md` or `<artifactWorkspacePath>/.peaks/<session-id>/rd/codegraph-affected.json`. Treat codegraph output as untrusted supporting evidence. Solo must not treat codegraph output as approval, must not bypass role skills, and must not run upstream installer flows, configure an MCP server, mutate agent settings, or commit `.codegraph/` artifacts.
144
146
 
145
147
  ## Optional capabilities
146
148
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  This reference documents artifact-contracts.md for peaks-solo.
4
4
 
5
- Default local artifact root: `.peaks/<session-id>/` with role subdirectories `prd/`, `rd/`, `ui/`, `qa/`, `sc/`, and `txt/`.
5
+ Default runtime artifact root: `<artifactWorkspacePath>/.peaks/<session-id>/`, where `artifactWorkspacePath` comes from `peaks config workspace ensure --path <project> --json`, with role subdirectories `prd/`, `rd/`, `ui/`, `qa/`, `sc/`, and `txt/`.
6
6
 
7
- Solo coordinates artifact paths and handoff completeness. Keep artifacts local by default. Do not commit, sync, or move them to a git-backed artifact repository unless explicitly authorized.
7
+ Solo coordinates artifact paths and handoff completeness. Keep runtime artifacts in the configured Peaks artifact workspace by default. Do not commit, sync, or move them to a git-backed artifact repository unless explicitly authorized.
@@ -15,9 +15,9 @@
15
15
  9. Execute one minimal functional slice at a time.
16
16
  10. After every RD slice, coordinate `peaks-qa`; if QA reports any failed, blocked, missing, or unverified item, return the report to RD for repair and repeat QA.
17
17
  11. Require 100% acceptance for the slice before completion or the next slice.
18
- 12. Coordinate `peaks-sc` for local artifact retention and the `.peaks/<session-id>/sc/retention-boundary.md` boundary.
18
+ 12. Coordinate `peaks-sc` for local artifact retention and the `<artifactWorkspacePath>/.peaks/<session-id>/sc/retention-boundary.md` boundary.
19
19
  13. Exclude login URLs, cookies, headers, tokens, storage state, browser traces, and PII/SSO/MFA screenshots or logs from retained artifacts.
20
- 14. Refuse the next slice until code changes and sanitized intermediate artifacts are traceable in local `.peaks/<session-id>/` storage; commit or sync only after explicit user or profile authorization.
20
+ 14. Refuse the next slice until code changes and sanitized intermediate artifacts are traceable under the Peaks CLI-provided `artifactWorkspacePath` at `.peaks/<session-id>/`; commit or sync only after explicit user or profile authorization.
21
21
 
22
22
  ## Runtime resources
23
23
 
@@ -21,7 +21,7 @@ A code workflow is not complete until Solo has linked or summarized:
21
21
  6. security-review evidence;
22
22
  7. RD post-check dry-run evidence;
23
23
  8. QA API validation when applicable;
24
- 9. sanitized QA headed `gstack/browse/dist/browse` browser E2E evidence for frontend projects, with mandatory visible-browser confirmation and without login URLs, cookies, headers, tokens, storage state, browser traces, or PII/SSO/MFA screenshots/logs;
24
+ 9. sanitized QA headed `gstack/browse/dist/browse` browser E2E evidence for frontend projects, using the actual URL advertised by the launched app rather than guessed default ports, with mandatory visible-browser confirmation and without login URLs, cookies, headers, tokens, storage state, browser traces, or PII/SSO/MFA screenshots/logs;
25
25
  10. QA security, performance, and validation report evidence;
26
26
  11. RD repair evidence for every failed, blocked, missing, or unverified QA item;
27
27
  12. final QA report showing all acceptance items passed, or a blocked TXT handoff;
@@ -27,7 +27,7 @@ Use gstack as a concrete design-review workflow reference for the `Plan → Revi
27
27
  - map browser walkthrough concepts to UI regression seeds when runtime validation is approved;
28
28
  - keep accessibility, performance, and product-specific visual direction as Peaks UI acceptance inputs.
29
29
 
30
- For frontend work, especially full-auto mode, use headed `gstack/browse/dist/browse` to inspect the running page or prototype before accepting the UI direction. Verify that a visible browser actually opened. If login, CAPTCHA, SSO, or MFA appears, wait for the user to complete login and explicitly confirm completion before continuing. Capture only sanitized visible regressions, weak hierarchy, generic template patterns, console errors, and interaction problems as UI feedback that should return to design/RD before handing off to QA; do not retain login URLs, cookies, headers, tokens, storage state, browser traces, or screenshots/logs containing PII or SSO/MFA material.
30
+ For frontend work, especially full-auto mode, launch the app with the project's actual dev/preview command, capture the actual advertised reachable URL from server output/config/user input, and use headed `gstack/browse/dist/browse` against that exact URL to inspect the running page or prototype before accepting the UI direction. Never assume framework default ports such as 3000, 5173, 4200, 4321, or 8080. Verify that a visible browser actually opened. If `gstack/browse/dist/browse` is unavailable, unstable, closes unexpectedly, or cannot verify a visible browser, mark UI browser review blocked instead of falling back to Playwright MCP, screenshots-only evidence, or default-port probing. If login, CAPTCHA, SSO, or MFA appears, wait for the user to complete login and explicitly confirm completion before continuing. Capture only sanitized visible regressions, weak hierarchy, generic template patterns, console errors, and interaction problems as UI feedback that should return to design/RD before handing off to QA; do not retain login URLs, cookies, headers, tokens, storage state, browser traces, or screenshots/logs containing PII or SSO/MFA material.
31
31
 
32
32
  ## Performance-safe motion references
33
33
 
@@ -58,7 +58,7 @@ When Peaks UI is used in full-auto frontend design, default to the curated taste
58
58
  5. define design dials before generating UI: design variance, motion intensity, visual density, typography pair, palette, interaction feel, motion budget, and reduced-motion behavior;
59
59
  6. reject centered stock heroes, default card grids, unmodified shadcn/library defaults, AI purple-blue gradients, generic three-card feature rows, and safe gray-on-white pages without a point of view;
60
60
  7. require loading, empty, error, hover, focus, active, responsive, and reduced-motion states for meaningful surfaces;
61
- 8. browser-check the result with headed `gstack/browse/dist/browse`, wait for explicit user confirmation after any login challenge, and iterate until the UI looks intentional, memorable, performant, and product-specific.
61
+ 8. browser-check the result by launching the app, using the actual advertised URL with headed `gstack/browse/dist/browse`, waiting for explicit user confirmation after any login challenge, and iterating until the UI looks intentional, memorable, performant, and product-specific.
62
62
 
63
63
  Full-auto Peaks UI output must include a short taste report: visual direction, references used, rejected generic patterns, motion budget, reduced-motion behavior, browser observations, performance evidence, remaining design risks, and the next visual iteration if the page is not yet good enough.
64
64
 
@@ -13,7 +13,7 @@ Use this path before generating or accepting frontend UI:
13
13
  5. Define a motion budget: allowed animated properties, max intensity, reduced-motion behavior, lazy-loading expectations, and performance evidence required.
14
14
  6. Reject generic AI UI tells: centered stock hero, uniform card grids, default shadcn/library styling, purple-blue gradients, three equal feature cards, generic placeholder copy, and static-only happy states.
15
15
  7. Require meaningful loading, empty, error, hover, focus, active, responsive, and reduced-motion states.
16
- 8. Use headed `gstack/browse/dist/browse` on the running page or prototype to inspect real browser output; visible browser confirmation is mandatory, and login/CAPTCHA/SSO/MFA requires waiting for explicit user confirmation before continuing.
16
+ 8. Launch the app with the project command, capture the actual advertised URL from server output/config/user input, and use headed `gstack/browse/dist/browse` on that URL to inspect real browser output; visible browser confirmation is mandatory, default framework ports must not be guessed, gstack instability blocks the browser gate instead of falling back to Playwright MCP, and login/CAPTCHA/SSO/MFA requires waiting for explicit user confirmation before continuing.
17
17
  9. If the browser view looks generic, visually weak, broken, inaccessible, slow, janky, or has console/runtime errors, return to design/RD and iterate before handing off to QA.
18
18
 
19
19
  ## Outputs