peaks-cli 1.0.13 → 1.0.14

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.
@@ -7,6 +7,7 @@ import { planProxyTest } from '../../services/proxy/proxy-service.js';
7
7
  import { runDoctor } from '../../services/doctor/doctor-service.js';
8
8
  import { listSkills } from '../../services/skills/skill-registry.js';
9
9
  import { inspectSkillRunbook } from '../../services/skills/skill-runbook-service.js';
10
+ import { setSkillPresence, clearSkillPresence, getSkillPresence } from '../../services/skills/skill-presence-service.js';
10
11
  import { fail, ok } from '../../shared/result.js';
11
12
  import { addJsonOption, failUnsupportedNonDryRun, getErrorMessage, isArtifactProvider, isArtifactSetupStep, printResult } from '../cli-helpers.js';
12
13
  export function registerCoreAndArtifactCommands(program, io) {
@@ -56,6 +57,30 @@ export function registerCoreAndArtifactCommands(program, io) {
56
57
  process.exitCode = 1;
57
58
  }
58
59
  });
60
+ addJsonOption(skill
61
+ .command('presence')
62
+ .description('Show the currently active Peaks skill')).action((options) => {
63
+ const presence = getSkillPresence();
64
+ if (presence === null) {
65
+ printResult(io, ok('skill.presence', { active: false }), options.json);
66
+ return;
67
+ }
68
+ printResult(io, ok('skill.presence', { active: true, ...presence }), options.json);
69
+ });
70
+ addJsonOption(skill
71
+ .command('presence:set <name>')
72
+ .description('Set the currently active Peaks skill for session-wide visibility')
73
+ .option('--mode <mode>', 'execution mode')
74
+ .option('--gate <gate>', 'current gate')).action((name, options) => {
75
+ const presence = setSkillPresence(name, options.mode, options.gate);
76
+ printResult(io, ok('skill.presence:set', { active: true, ...presence }), options.json);
77
+ });
78
+ addJsonOption(skill
79
+ .command('presence:clear')
80
+ .description('Clear the active Peaks skill presence indicator')).action((options) => {
81
+ const removed = clearSkillPresence();
82
+ printResult(io, ok('skill.presence:clear', { active: false, removed }), options.json);
83
+ });
59
84
  const profile = program.command('profile').description('Manage runtime profiles');
60
85
  addJsonOption(profile.command('list').description('List available profiles')).action((options) => {
61
86
  printResult(io, ok('profile.list', { profiles: listProfiles() }), options.json);
@@ -0,0 +1,2 @@
1
+ import type { CodegraphExecutionResult, CodegraphInvocation } from './codegraph-service.js';
2
+ export declare function defaultCodegraphProcessRunner(invocation: CodegraphInvocation): Promise<CodegraphExecutionResult>;
@@ -0,0 +1,93 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { join } from 'node:path';
3
+ const CODEGRAPH_PROCESS_TIMEOUT_MS = 600_000;
4
+ const CODEGRAPH_OUTPUT_LIMIT_BYTES = 10 * 1024 * 1024;
5
+ function createCodegraphEnvironment(sourceEnv = process.env) {
6
+ const preservedKeys = ['PATH', 'Path', 'HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'TEMP', 'TMP', 'SystemRoot', 'WINDIR'];
7
+ const environment = {};
8
+ for (const key of preservedKeys) {
9
+ const value = sourceEnv[key];
10
+ if (value !== undefined) {
11
+ environment[key] = value;
12
+ }
13
+ }
14
+ return environment;
15
+ }
16
+ function assertOutputLimit(currentSize, chunkSize) {
17
+ const nextSize = currentSize + chunkSize;
18
+ if (nextSize > CODEGRAPH_OUTPUT_LIMIT_BYTES) {
19
+ throw new Error(`codegraph output exceeded ${CODEGRAPH_OUTPUT_LIMIT_BYTES} bytes`);
20
+ }
21
+ return nextSize;
22
+ }
23
+ function terminateCodegraphProcess(childProcess) {
24
+ if (childProcess.pid === undefined) {
25
+ childProcess.kill();
26
+ return;
27
+ }
28
+ if (process.platform === 'win32') {
29
+ if (process.env.SystemRoot) {
30
+ spawn(join(process.env.SystemRoot, 'System32', 'taskkill.exe'), ['/pid', String(childProcess.pid), '/T', '/F'], { shell: false, stdio: 'ignore' });
31
+ }
32
+ else {
33
+ childProcess.kill();
34
+ }
35
+ return;
36
+ }
37
+ try {
38
+ process.kill(-childProcess.pid, 'SIGTERM');
39
+ }
40
+ catch {
41
+ childProcess.kill('SIGTERM');
42
+ }
43
+ }
44
+ export function defaultCodegraphProcessRunner(invocation) {
45
+ return new Promise((resolveResult, reject) => {
46
+ const childProcess = spawn(invocation.executable, invocation.args, {
47
+ cwd: invocation.cwd,
48
+ detached: process.platform !== 'win32',
49
+ env: createCodegraphEnvironment(),
50
+ shell: false
51
+ });
52
+ const timeout = setTimeout(() => {
53
+ terminateCodegraphProcess(childProcess);
54
+ reject(new Error(`codegraph process timed out after ${CODEGRAPH_PROCESS_TIMEOUT_MS}ms`));
55
+ }, CODEGRAPH_PROCESS_TIMEOUT_MS);
56
+ const stdoutChunks = [];
57
+ const stderrChunks = [];
58
+ let stdoutSize = 0;
59
+ let stderrSize = 0;
60
+ childProcess.stdout.on('data', (chunk) => {
61
+ try {
62
+ stdoutSize = assertOutputLimit(stdoutSize, chunk.length);
63
+ stdoutChunks.push(chunk);
64
+ }
65
+ catch (error) {
66
+ terminateCodegraphProcess(childProcess);
67
+ reject(error);
68
+ }
69
+ });
70
+ childProcess.stderr.on('data', (chunk) => {
71
+ try {
72
+ stderrSize = assertOutputLimit(stderrSize, chunk.length);
73
+ stderrChunks.push(chunk);
74
+ }
75
+ catch (error) {
76
+ terminateCodegraphProcess(childProcess);
77
+ reject(error);
78
+ }
79
+ });
80
+ childProcess.on('error', (error) => {
81
+ clearTimeout(timeout);
82
+ reject(error);
83
+ });
84
+ childProcess.on('close', (exitCode) => {
85
+ clearTimeout(timeout);
86
+ resolveResult({
87
+ exitCode,
88
+ stdout: Buffer.concat(stdoutChunks).toString('utf8'),
89
+ stderr: Buffer.concat(stderrChunks).toString('utf8')
90
+ });
91
+ });
92
+ });
93
+ }
@@ -1,16 +1,11 @@
1
1
  import { existsSync, realpathSync, statSync } from 'node:fs';
2
- import { spawn } from 'node:child_process';
3
2
  import { createRequire } from 'node:module';
4
- import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
3
+ import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
4
+ import { defaultCodegraphProcessRunner } from './codegraph-process-runner.js';
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
- const CODEGRAPH_PROCESS_TIMEOUT_MS = 600_000;
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
9
  const POSITIONAL_ARGUMENT_PREFIX = '-';
15
10
  const ALLOWED_SUBCOMMANDS = ['status', 'init', 'index', 'query', 'files', 'context', 'affected'];
16
11
  const NUMERIC_FLAG_NAMES = ['limit', 'maxDepth'];
@@ -28,9 +23,6 @@ function resolveCodegraphBinaryPath() {
28
23
  const require = createRequire(import.meta.url);
29
24
  const packageJsonPath = require.resolve('@colbymchenry/codegraph/package.json');
30
25
  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
- }
34
26
  return binaryPath;
35
27
  }
36
28
  function assertSupportedSubcommand(subcommand) {
@@ -95,9 +87,6 @@ function resolveExistingBoundary(absoluteFilePath) {
95
87
  let currentPath = dirname(absoluteFilePath);
96
88
  while (!existsSync(currentPath)) {
97
89
  const parentPath = dirname(currentPath);
98
- if (parentPath === currentPath) {
99
- return currentPath;
100
- }
101
90
  currentPath = parentPath;
102
91
  }
103
92
  return currentPath;
@@ -150,91 +139,6 @@ function buildCommandArgs(options, projectRoot) {
150
139
  }
151
140
  return args;
152
141
  }
153
- function createCodegraphEnvironment(sourceEnv = process.env) {
154
- const preservedKeys = ['PATH', 'Path', 'HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'TEMP', 'TMP', 'SystemRoot', 'WINDIR'];
155
- const environment = {};
156
- for (const key of preservedKeys) {
157
- const value = sourceEnv[key];
158
- if (value !== undefined) {
159
- environment[key] = value;
160
- }
161
- }
162
- return environment;
163
- }
164
- function assertOutputLimit(currentSize, chunkSize) {
165
- const nextSize = currentSize + chunkSize;
166
- if (nextSize > CODEGRAPH_OUTPUT_LIMIT_BYTES) {
167
- throw new Error(`codegraph output exceeded ${CODEGRAPH_OUTPUT_LIMIT_BYTES} bytes`);
168
- }
169
- return nextSize;
170
- }
171
- function terminateCodegraphProcess(childProcess) {
172
- if (childProcess.pid === undefined) {
173
- childProcess.kill();
174
- return;
175
- }
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' });
179
- return;
180
- }
181
- try {
182
- process.kill(-childProcess.pid, 'SIGTERM');
183
- }
184
- catch {
185
- childProcess.kill('SIGTERM');
186
- }
187
- }
188
- function defaultCodegraphProcessRunner(invocation) {
189
- return new Promise((resolveResult, reject) => {
190
- const childProcess = spawn(invocation.executable, invocation.args, {
191
- cwd: invocation.cwd,
192
- detached: process.platform !== 'win32',
193
- env: createCodegraphEnvironment(),
194
- shell: false
195
- });
196
- const timeout = setTimeout(() => {
197
- terminateCodegraphProcess(childProcess);
198
- reject(new Error(`codegraph process timed out after ${CODEGRAPH_PROCESS_TIMEOUT_MS}ms`));
199
- }, CODEGRAPH_PROCESS_TIMEOUT_MS);
200
- const stdoutChunks = [];
201
- const stderrChunks = [];
202
- let stdoutSize = 0;
203
- let stderrSize = 0;
204
- childProcess.stdout.on('data', (chunk) => {
205
- try {
206
- stdoutSize = assertOutputLimit(stdoutSize, chunk.length);
207
- stdoutChunks.push(chunk);
208
- }
209
- catch (error) {
210
- terminateCodegraphProcess(childProcess);
211
- reject(error);
212
- }
213
- });
214
- childProcess.stderr.on('data', (chunk) => {
215
- try {
216
- stderrSize = assertOutputLimit(stderrSize, chunk.length);
217
- stderrChunks.push(chunk);
218
- }
219
- catch (error) {
220
- terminateCodegraphProcess(childProcess);
221
- reject(error);
222
- }
223
- });
224
- childProcess.on('error', (error) => {
225
- clearTimeout(timeout);
226
- reject(error);
227
- });
228
- childProcess.on('close', (exitCode) => {
229
- clearTimeout(timeout);
230
- resolveResult({
231
- exitCode,
232
- stdout: Buffer.concat(stdoutChunks).toString('utf8'),
233
- stderr: Buffer.concat(stderrChunks).toString('utf8')
234
- });
235
- });
236
- });
237
- }
238
142
  export function createCodegraphInvocation(options) {
239
143
  assertSupportedSubcommand(options.subcommand);
240
144
  const projectRoot = resolveProjectRoot(options.project);
@@ -74,7 +74,7 @@ function buildServerConfig(name, raw, scope, source, pluginName) {
74
74
  scope,
75
75
  source
76
76
  };
77
- if (pluginName !== undefined) {
77
+ if (pluginName !== undefined && pluginName.length > 0) {
78
78
  config.pluginName = pluginName;
79
79
  }
80
80
  return config;
@@ -0,0 +1,10 @@
1
+ export type SkillPresence = {
2
+ skill: string;
3
+ mode?: string;
4
+ gate?: string;
5
+ setAt: string;
6
+ };
7
+ export declare function exportSkillPresence(): string;
8
+ export declare function setSkillPresence(skill: string, mode?: string, gate?: string): SkillPresence;
9
+ export declare function getSkillPresence(): SkillPresence | null;
10
+ export declare function clearSkillPresence(): boolean;
@@ -0,0 +1,54 @@
1
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ const PRESENCE_FILE = '.peaks/.active-skill.json';
4
+ function resolvePresencePath() {
5
+ return resolve(process.cwd(), PRESENCE_FILE);
6
+ }
7
+ export function exportSkillPresence() {
8
+ return resolvePresencePath();
9
+ }
10
+ export function setSkillPresence(skill, mode, gate) {
11
+ const presence = {
12
+ skill,
13
+ ...(mode ? { mode } : {}),
14
+ ...(gate ? { gate } : {}),
15
+ setAt: new Date().toISOString()
16
+ };
17
+ const presencePath = resolvePresencePath();
18
+ const presenceDir = dirname(presencePath);
19
+ if (!existsSync(presenceDir)) {
20
+ mkdirSync(presenceDir, { recursive: true });
21
+ }
22
+ writeFileSync(presencePath, JSON.stringify(presence, null, 2), 'utf8');
23
+ return presence;
24
+ }
25
+ export function getSkillPresence() {
26
+ const presencePath = resolvePresencePath();
27
+ if (!existsSync(presencePath)) {
28
+ return null;
29
+ }
30
+ try {
31
+ const raw = readFileSync(presencePath, 'utf8');
32
+ const parsed = JSON.parse(raw);
33
+ if (typeof parsed?.skill !== 'string' || parsed.skill.length === 0) {
34
+ return null;
35
+ }
36
+ return parsed;
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ export function clearSkillPresence() {
43
+ const presencePath = resolvePresencePath();
44
+ if (!existsSync(presencePath)) {
45
+ return false;
46
+ }
47
+ try {
48
+ unlinkSync(presencePath);
49
+ return true;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
@@ -11,7 +11,7 @@ const AUTHORIZATION_KEYWORDS_PATTERN = /authoriz|explicit|--dry-run|approv|only
11
11
  const PEAKS_COMMAND_LINE_PATTERN = /^\s*peaks\s+\w/;
12
12
  function extractRunbookSection(body) {
13
13
  const match = /## Default runbook\n+([\s\S]*?)(?=\n## |$)/.exec(body);
14
- return match === null ? null : (match[1] ?? null);
14
+ return match === null ? null : match[1];
15
15
  }
16
16
  function findDestructiveApplyLines(section) {
17
17
  const lines = section.split(/\r?\n/);
@@ -250,9 +250,6 @@ function readResumeArtifact(artifactWorkspacePath, artifact) {
250
250
  return null;
251
251
  }
252
252
  const pathSegments = artifact.replace(/\\/g, '/').split('/');
253
- if (pathSegments.length < 4 || pathSegments[0] !== '.peaks') {
254
- return null;
255
- }
256
253
  const sessionRootPath = resolve(artifactWorkspacePath, '.peaks', pathSegments[1]);
257
254
  const roleRootPath = resolve(sessionRootPath, pathSegments[2]);
258
255
  if (lstatSync(sessionRootPath).isSymbolicLink() || lstatSync(roleRootPath).isSymbolicLink()) {
@@ -261,16 +258,13 @@ function readResumeArtifact(artifactWorkspacePath, artifact) {
261
258
  let allowedRootRealPath;
262
259
  if (pathSegments[2] === 'rd') {
263
260
  const swarmRootPath = resolve(roleRootPath, 'swarm');
264
- if (pathSegments[3] !== 'swarm' || lstatSync(swarmRootPath).isSymbolicLink()) {
261
+ if (lstatSync(swarmRootPath).isSymbolicLink()) {
265
262
  return null;
266
263
  }
267
264
  allowedRootRealPath = realpathSync(swarmRootPath);
268
265
  }
269
- else if (pathSegments[2] === 'prd') {
270
- allowedRootRealPath = realpathSync(roleRootPath);
271
- }
272
266
  else {
273
- return null;
267
+ allowedRootRealPath = realpathSync(roleRootPath);
274
268
  }
275
269
  const artifactRealPath = realpathSync(artifactPath);
276
270
  if (!isInsidePath(allowedRootRealPath, artifactWorkspaceRealPath) || !isInsidePath(artifactRealPath, allowedRootRealPath)) {
@@ -354,12 +348,12 @@ function parseFrontMatter(content) {
354
348
  if (lines[0] !== '---') {
355
349
  return null;
356
350
  }
357
- const endIndex = lines.slice(1).findIndex((line) => line === '---');
358
- if (endIndex === -1) {
351
+ const closingDelimiterIndex = lines.slice(1).findIndex((line) => line === '---');
352
+ if (closingDelimiterIndex === -1) {
359
353
  return null;
360
354
  }
361
355
  const metadata = new Map();
362
- for (const line of lines.slice(1, endIndex + 1)) {
356
+ for (const line of lines.slice(1, closingDelimiterIndex + 1)) {
363
357
  const separatorIndex = line.indexOf(':');
364
358
  if (separatorIndex === -1) {
365
359
  return null;
@@ -370,8 +364,8 @@ function parseFrontMatter(content) {
370
364
  }
371
365
  function getMarkdownBody(content) {
372
366
  const lines = content.split(/\r?\n/);
373
- const endIndex = lines.slice(1).findIndex((line) => line === '---');
374
- return endIndex === -1 ? '' : lines.slice(endIndex + 2).join('\n');
367
+ const closingDelimiterIndex = lines.slice(1).findIndex((line) => line === '---');
368
+ return closingDelimiterIndex === -1 ? '' : lines.slice(closingDelimiterIndex + 2).join('\n');
375
369
  }
376
370
  function hasValidationReportBody(body) {
377
371
  return body.includes('Validation summary:')
@@ -1 +1 @@
1
- export declare const CLI_VERSION = "1.0.13";
1
+ export declare const CLI_VERSION = "1.0.14";
@@ -1 +1 @@
1
- export const CLI_VERSION = "1.0.13";
1
+ export const CLI_VERSION = "1.0.14";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-cli",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "Peaks CLI and short skill family for Claude Code automation.",
5
5
  "author": "SquabbyZ",
6
6
  "license": "MIT",
@@ -34,6 +34,7 @@ For a feature / bug / clarification request with no authenticated source documen
34
34
  ```bash
35
35
  # 0. confirm PRD's own runbook integrity before driving any phase
36
36
  peaks skill runbook peaks-prd --json
37
+ peaks skill presence:set peaks-prd # show persistent skill presence every turn
37
38
 
38
39
  # 1. capture the request as the canonical PRD artifact (preview, then apply)
39
40
  peaks request init --role prd --id <request-id> --project <repo> --json
@@ -53,6 +54,7 @@ peaks codegraph status --project <repo> # local index status
53
54
 
54
55
  # 5. write goals / non-goals / acceptance into the artifact body, then hand off
55
56
  peaks request show <request-id> --role prd --project <repo> --json
57
+ peaks skill presence:clear # handoff complete, remove presence indicator
56
58
  ```
57
59
 
58
60
  For an authenticated product document request (Feishu/Lark/wiki), add before step 5:
@@ -33,6 +33,7 @@ The default sequence the QA skill should execute. Do not skip the boundary check
33
33
  ```bash
34
34
  # 0. confirm QA's own runbook integrity before validating anything
35
35
  peaks skill runbook peaks-qa --json
36
+ peaks skill presence:set peaks-qa # show persistent skill presence every turn
36
37
 
37
38
  # 1. capture the QA request artifact and read upstream scope
38
39
  peaks request init --role qa --id <request-id> --project <repo> --apply --json
@@ -69,6 +70,7 @@ peaks mcp apply --capability playwright-mcp.browser-validation --yes --json
69
70
  # 7. on verdict=return-to-rd, route findings back through the request id; otherwise close.
70
71
  peaks request show <request-id> --role qa --project <repo> --json
71
72
  peaks openspec archive <change-id> --project <repo> --json # preview, then --apply on full pass
73
+ peaks skill presence:clear # QA complete, remove presence indicator
72
74
  ```
73
75
 
74
76
  Verdict `pass` is blocked until every applicable validation gate has evidence in the artifact.
@@ -31,6 +31,7 @@ The default sequence the RD skill should execute for a code-touching request. Sk
31
31
  ```bash
32
32
  # 0. confirm RD's own runbook integrity before any code edit
33
33
  peaks skill runbook peaks-rd --json
34
+ peaks skill presence:set peaks-rd # show persistent skill presence every turn
34
35
 
35
36
  # 1. capture the RD request artifact and read upstream PRD / UI scope
36
37
  peaks request init --role rd --id <request-id> --project <repo> --apply --json
@@ -65,6 +66,7 @@ peaks openspec validate <change-id> --project <repo> --json # exit gate (re-r
65
66
  # 8. hand off to QA via the cross-linked request id
66
67
  peaks request init --role qa --id <request-id> --project <repo> --apply --json
67
68
  peaks request show <request-id> --role rd --project <repo> --json
69
+ peaks skill presence:clear # handoff complete, remove presence indicator
68
70
  ```
69
71
 
70
72
  For refactor work, the coverage ≥ 95% gate in `Refactor hard gates` still applies and must be recorded in the artifact before slicing begins.
@@ -48,6 +48,7 @@ Use this sequence when SC owns the change-control pass for a refactor or release
48
48
  ```bash
49
49
  # 0. Confirm SC's own runbook integrity before recording boundary evidence
50
50
  peaks skill runbook peaks-sc --json
51
+ peaks skill presence:set peaks-sc # show persistent skill presence every turn
51
52
 
52
53
  # 1. Derive commit boundaries from OpenSpec when openspec/ exists
53
54
  peaks openspec to-rd <change-id> --project <repo> --json
@@ -71,6 +72,7 @@ peaks sc boundary --slice-id <slice-id> --artifact <artifact-path> --code <code-
71
72
  # 7. Sync memory and artifacts only when the user or active profile authorizes durable writes
72
73
  peaks memory sync --project <repo> --workspace <workspace> --apply --json
73
74
  peaks artifacts sync --workspace <workspace> --apply --json
75
+ peaks skill presence:clear # SC complete, remove presence indicator
74
76
  ```
75
77
 
76
78
  The final two `--apply` calls require explicit authorization. Without it, default to `--dry-run` or omit the sync calls entirely and keep the boundary evidence local under `.peaks/<session-id>/`.
@@ -92,6 +92,7 @@ The default end-to-end sequence Peaks Solo orchestrates when a user supplies a r
92
92
  peaks doctor --json
93
93
  peaks project dashboard --project <repo> --json # one-call cross-role status
94
94
  peaks skill runbook peaks-solo --json # confirm Solo's own runbook is intact + apply-gated
95
+ peaks skill presence:set peaks-solo --mode solo # show persistent skill presence every turn
95
96
 
96
97
  # 1. PRD phase — capture the request as the canonical artifact
97
98
  peaks request init --role prd --id <request-id> --project <repo> --apply --json
@@ -141,6 +142,7 @@ peaks memory extract --project <repo> --artifact <qa-artifact> --dry-run --json
141
142
  # 8. final snapshot to confirm the workflow really closed
142
143
  peaks project dashboard --project <repo> --json
143
144
  peaks skill doctor --json # all 7 required skills still healthy?
145
+ peaks skill presence:clear # workflow complete, remove presence indicator
144
146
  ```
145
147
 
146
148
  Solo's RD↔QA repair loop (`## Mandatory RD QA repair loop` above) applies if QA's verdict is `return-to-rd`. In that case, Solo re-runs phase 3 + phase 4 against the same `<request-id>` instead of starting a new one; the previous artifacts get appended with new transition notes via `--reason` rather than rewritten.
@@ -105,6 +105,7 @@ Use this sequence when TXT compresses an in-flight workflow into a portable, com
105
105
  ```bash
106
106
  # 0. Confirm TXT's own runbook integrity before compressing a handoff
107
107
  peaks skill runbook peaks-txt --json
108
+ peaks skill presence:set peaks-txt # show persistent skill presence every turn
108
109
 
109
110
  # 1. Inventory per-role artifacts already produced for the request
110
111
  peaks request list --project <repo> --json
@@ -123,6 +124,7 @@ peaks capabilities --json
123
124
  # 5. Memory extraction — dry-run by default, apply only when authorized
124
125
  peaks memory extract --project <repo> --artifact <artifact-path> --dry-run --json
125
126
  peaks memory extract --project <repo> --artifact <artifact-path> --apply --json
127
+ peaks skill presence:clear # handoff capsule complete, remove presence indicator
126
128
  ```
127
129
 
128
130
  The final `--apply` call requires explicit user or profile authorization. Without it, keep the capsule under `.peaks/<session-id>/txt/` and reference artifact paths from other roles instead of duplicating their content.
@@ -30,6 +30,7 @@ The default sequence the UI skill should execute. Skip steps that do not apply;
30
30
  ```bash
31
31
  # 0. confirm UI's own runbook integrity before driving any phase
32
32
  peaks skill runbook peaks-ui --json
33
+ peaks skill presence:set peaks-ui # show persistent skill presence every turn
33
34
 
34
35
  # 1. capture the UI request as a durable artifact tied to the same PRD request id
35
36
  peaks request init --role ui --id <request-id> --project <repo> --json
@@ -55,6 +56,7 @@ peaks mcp apply --capability playwright-mcp.browser-validation --yes --json #
55
56
  # 5. hand off to RD / QA via the cross-linked request id
56
57
  peaks request list --project <repo> --json
57
58
  peaks request show <request-id> --role ui --project <repo> --json
59
+ peaks skill presence:clear # handoff complete, remove presence indicator
58
60
  ```
59
61
 
60
62
  Handoff is blocked until the UI artifact's `state` reaches `direction-locked` or `handed-off`.