peaks-cli 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/peaks.js +0 -0
- package/dist/src/cli/commands/request-commands.js +13 -1
- package/dist/src/cli/commands/statusline-commands.d.ts +3 -0
- package/dist/src/cli/commands/statusline-commands.js +111 -0
- package/dist/src/cli/program.js +2 -0
- package/dist/src/services/artifacts/artifact-lint-service.js +20 -1
- package/dist/src/services/artifacts/artifact-prerequisites.js +34 -4
- package/dist/src/services/doctor/doctor-service.d.ts +1 -0
- package/dist/src/services/doctor/doctor-service.js +40 -0
- package/dist/src/services/scan/type-sanity-service.js +11 -1
- package/dist/src/services/skills/skill-statusline-renderer.d.ts +6 -0
- package/dist/src/services/skills/skill-statusline-renderer.js +55 -0
- package/dist/src/services/skills/skill-statusline-service.d.ts +22 -0
- package/dist/src/services/skills/skill-statusline-service.js +94 -0
- package/dist/src/services/skills/statusline-settings-service.d.ts +32 -0
- package/dist/src/services/skills/statusline-settings-service.js +144 -0
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/schemas/doctor-report.schema.json +2 -2
- package/skills/peaks-prd/SKILL.md +7 -0
- package/skills/peaks-qa/SKILL.md +7 -0
- package/skills/peaks-rd/SKILL.md +7 -0
- package/skills/peaks-sc/SKILL.md +7 -0
- package/skills/peaks-solo/SKILL.md +6 -0
- package/skills/peaks-txt/SKILL.md +7 -0
- package/skills/peaks-ui/SKILL.md +7 -0
package/bin/peaks.js
CHANGED
|
File without changes
|
|
@@ -126,6 +126,18 @@ export function registerRequestCommands(program, io) {
|
|
|
126
126
|
try {
|
|
127
127
|
const role = options.role;
|
|
128
128
|
const newState = parseStateForRole(role, options.state);
|
|
129
|
+
// Resolve the artifact's real session up front. Falling back to a literal
|
|
130
|
+
// 'default' (the previous behavior) points the bypass counter at a
|
|
131
|
+
// non-existent .peaks/default/ dir and crashes with ENOENT, so when
|
|
132
|
+
// --session-id is omitted we look the artifact up to find its session.
|
|
133
|
+
let resolvedSessionId = options.sessionId;
|
|
134
|
+
if (resolvedSessionId === undefined) {
|
|
135
|
+
const { showRequestArtifact: showForSession } = await import('../../services/artifacts/request-artifact-service.js');
|
|
136
|
+
const located = await showForSession({ projectRoot: options.project, role, requestId });
|
|
137
|
+
if (located !== null) {
|
|
138
|
+
resolvedSessionId = located.sessionId;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
129
141
|
if (options.allowIncomplete === true && (options.reason === undefined || options.reason.trim().length === 0)) {
|
|
130
142
|
printResult(io, fail('request.transition', 'BYPASS_REASON_REQUIRED', '--allow-incomplete requires --reason explaining why prerequisites are skipped', { role, requestId }, ['Add --reason "<short justification>" or remove --allow-incomplete and produce the missing artifacts']), options.json);
|
|
131
143
|
process.exitCode = 1;
|
|
@@ -142,7 +154,7 @@ export function registerRequestCommands(program, io) {
|
|
|
142
154
|
return;
|
|
143
155
|
}
|
|
144
156
|
// Check bypass count
|
|
145
|
-
const sessionRoot = (await import('node:path')).join(options.project, '.peaks',
|
|
157
|
+
const sessionRoot = (await import('node:path')).join(options.project, '.peaks', resolvedSessionId ?? 'default');
|
|
146
158
|
if (isBypassLimitReached(sessionRoot)) {
|
|
147
159
|
printResult(io, fail('request.transition', 'BYPASS_LIMIT_REACHED', `--allow-incomplete limit reached (${MAX_BYPASSES_PER_SESSION} per session)`, { role, requestId, limit: MAX_BYPASSES_PER_SESSION }, ['Produce the missing artifacts instead of bypassing.']), options.json);
|
|
148
160
|
process.exitCode = 1;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { fail, ok } from '../../shared/result.js';
|
|
2
|
+
import { addJsonOption, printResult, getErrorMessage } from '../cli-helpers.js';
|
|
3
|
+
import { findProjectRoot } from '../../services/config/config-safety.js';
|
|
4
|
+
import { buildStatusLineModel, parseStatusLineStdin } from '../../services/skills/skill-statusline-service.js';
|
|
5
|
+
import { renderStatusLine } from '../../services/skills/skill-statusline-renderer.js';
|
|
6
|
+
import { applyStatusLineInstall, planStatusLineInstall, removeStatusLineInstall } from '../../services/skills/statusline-settings-service.js';
|
|
7
|
+
const STDIN_READ_TIMEOUT_MS = 250;
|
|
8
|
+
/** Read piped stdin if present; resolve quickly with '' when attached to a TTY. */
|
|
9
|
+
function readStdin() {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
if (process.stdin.isTTY) {
|
|
12
|
+
resolve('');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
let data = '';
|
|
16
|
+
let settled = false;
|
|
17
|
+
const finish = () => {
|
|
18
|
+
if (settled)
|
|
19
|
+
return;
|
|
20
|
+
settled = true;
|
|
21
|
+
resolve(data);
|
|
22
|
+
};
|
|
23
|
+
const timer = setTimeout(finish, STDIN_READ_TIMEOUT_MS);
|
|
24
|
+
timer.unref?.();
|
|
25
|
+
process.stdin.setEncoding('utf8');
|
|
26
|
+
process.stdin.on('data', (chunk) => {
|
|
27
|
+
data += chunk;
|
|
28
|
+
});
|
|
29
|
+
process.stdin.on('end', () => {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
finish();
|
|
32
|
+
});
|
|
33
|
+
process.stdin.on('error', () => {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
finish();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function resolveScope(options) {
|
|
40
|
+
return options.global ? 'global' : 'project';
|
|
41
|
+
}
|
|
42
|
+
export function registerStatusLineCommands(program, io) {
|
|
43
|
+
const statusline = program
|
|
44
|
+
.command('statusline')
|
|
45
|
+
.description('Render the Peaks skill status line for Claude Code (reads session JSON on stdin)')
|
|
46
|
+
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
|
|
47
|
+
.action(async (options) => {
|
|
48
|
+
const raw = await readStdin();
|
|
49
|
+
const stdin = parseStatusLineStdin(raw);
|
|
50
|
+
// When a project override is passed (or no stdin), seed cwd so detection works.
|
|
51
|
+
const seeded = options.project
|
|
52
|
+
? { ...(stdin ?? {}), workspace: { current_dir: options.project } }
|
|
53
|
+
: stdin;
|
|
54
|
+
const model = buildStatusLineModel(seeded, Date.now());
|
|
55
|
+
io.stdout(renderStatusLine(model));
|
|
56
|
+
});
|
|
57
|
+
addJsonOption(statusline
|
|
58
|
+
.command('install')
|
|
59
|
+
.description('Install the Peaks status line into .claude/settings.json (project scope by default)')
|
|
60
|
+
.option('--global', 'install into the user-level ~/.claude/settings.json instead of the project')
|
|
61
|
+
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
|
|
62
|
+
.option('--force', 'overwrite an existing non-Peaks statusLine entry')
|
|
63
|
+
.option('--dry-run', 'show what would change without writing')).action((options) => {
|
|
64
|
+
const scope = resolveScope(options);
|
|
65
|
+
const projectRoot = scope === 'project'
|
|
66
|
+
? (options.project ?? findProjectRoot(process.cwd()) ?? process.cwd())
|
|
67
|
+
: undefined;
|
|
68
|
+
try {
|
|
69
|
+
if (options.dryRun) {
|
|
70
|
+
const plan = planStatusLineInstall(scope, projectRoot);
|
|
71
|
+
const warnings = plan.conflict
|
|
72
|
+
? [`An existing statusLine command is set: ${plan.conflictCommand}. Rerun with --force to overwrite.`]
|
|
73
|
+
: [];
|
|
74
|
+
printResult(io, ok('statusline.install', { ...plan, applied: false, dryRun: true }, warnings), options.json);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const result = applyStatusLineInstall(scope, projectRoot, options.force ? { force: true } : {});
|
|
78
|
+
const warnings = result.conflict && !result.applied
|
|
79
|
+
? [`An existing statusLine command is set: ${result.conflictCommand}. Rerun with --force to overwrite.`]
|
|
80
|
+
: [];
|
|
81
|
+
const nextActions = result.applied
|
|
82
|
+
? ['Restart Claude Code (or reload the window) so the status line takes effect']
|
|
83
|
+
: [];
|
|
84
|
+
printResult(io, ok('statusline.install', { ...result, dryRun: false }, warnings, nextActions), options.json);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const message = getErrorMessage(error);
|
|
88
|
+
printResult(io, fail('statusline.install', 'STATUSLINE_INSTALL_FAILED', message, { scope, applied: false }, [message]), options.json);
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
addJsonOption(statusline
|
|
93
|
+
.command('uninstall')
|
|
94
|
+
.description('Remove the Peaks status line from .claude/settings.json')
|
|
95
|
+
.option('--global', 'remove from the user-level ~/.claude/settings.json instead of the project')
|
|
96
|
+
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')).action((options) => {
|
|
97
|
+
const scope = resolveScope(options);
|
|
98
|
+
const projectRoot = scope === 'project'
|
|
99
|
+
? (options.project ?? findProjectRoot(process.cwd()) ?? process.cwd())
|
|
100
|
+
: undefined;
|
|
101
|
+
try {
|
|
102
|
+
const result = removeStatusLineInstall(scope, projectRoot);
|
|
103
|
+
printResult(io, ok('statusline.uninstall', result), options.json);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const message = getErrorMessage(error);
|
|
107
|
+
printResult(io, fail('statusline.uninstall', 'STATUSLINE_UNINSTALL_FAILED', message, { scope, removed: false }, [message]), options.json);
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
package/dist/src/cli/program.js
CHANGED
|
@@ -10,6 +10,7 @@ import { registerProjectCommands } from './commands/project-commands.js';
|
|
|
10
10
|
import { registerRequestCommands } from './commands/request-commands.js';
|
|
11
11
|
import { registerScanCommands } from './commands/scan-commands.js';
|
|
12
12
|
import { registerShadcnCommands } from './commands/shadcn-commands.js';
|
|
13
|
+
import { registerStatusLineCommands } from './commands/statusline-commands.js';
|
|
13
14
|
import { registerUnderstandCommands } from './commands/understand-commands.js';
|
|
14
15
|
import { registerWorkspaceCommands } from './commands/workspace-commands.js';
|
|
15
16
|
export { printResult } from './cli-helpers.js';
|
|
@@ -40,6 +41,7 @@ export function createProgram(io = { stdout: (text) => console.log(text), stderr
|
|
|
40
41
|
registerRequestCommands(program, io);
|
|
41
42
|
registerScanCommands(program, io);
|
|
42
43
|
registerShadcnCommands(program, io);
|
|
44
|
+
registerStatusLineCommands(program, io);
|
|
43
45
|
registerUnderstandCommands(program, io);
|
|
44
46
|
registerWorkspaceCommands(program, io);
|
|
45
47
|
return program;
|
|
@@ -35,6 +35,15 @@ const ALLOWLIST_PATTERNS = [
|
|
|
35
35
|
function isAllowlisted(line) {
|
|
36
36
|
return ALLOWLIST_PATTERNS.some((pattern) => pattern.test(line));
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Remove inline code spans (`...`) before applying placeholder rules. Content
|
|
40
|
+
* inside backticks is literal example text — e.g. a documented command syntax
|
|
41
|
+
* `peaks sop init <id>` — not an unfilled prose placeholder. Lint checks prose,
|
|
42
|
+
* not code, so a `<...>` token only counts when it appears outside code spans.
|
|
43
|
+
*/
|
|
44
|
+
function stripInlineCode(line) {
|
|
45
|
+
return line.replace(/`[^`]*`/g, '');
|
|
46
|
+
}
|
|
38
47
|
export async function lintRequestArtifact(options) {
|
|
39
48
|
const showOptions = {
|
|
40
49
|
projectRoot: options.projectRoot,
|
|
@@ -50,14 +59,24 @@ export async function lintRequestArtifact(options) {
|
|
|
50
59
|
}
|
|
51
60
|
const lines = artifact.content.split(/\r?\n/);
|
|
52
61
|
const findings = [];
|
|
62
|
+
let insideFence = false;
|
|
53
63
|
for (let index = 0; index < lines.length; index += 1) {
|
|
54
64
|
const rawLine = lines[index];
|
|
55
65
|
if (rawLine === undefined)
|
|
56
66
|
continue;
|
|
67
|
+
// Fenced code blocks hold literal examples, not prose to fill; skip their
|
|
68
|
+
// contents entirely (the fence delimiters themselves toggle the state).
|
|
69
|
+
if (/^\s*```/.test(rawLine)) {
|
|
70
|
+
insideFence = !insideFence;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (insideFence)
|
|
74
|
+
continue;
|
|
57
75
|
if (isAllowlisted(rawLine))
|
|
58
76
|
continue;
|
|
77
|
+
const testLine = stripInlineCode(rawLine);
|
|
59
78
|
for (const rule of RULES) {
|
|
60
|
-
if (rule.test(
|
|
79
|
+
if (rule.test(testLine)) {
|
|
61
80
|
findings.push({
|
|
62
81
|
line: index + 1,
|
|
63
82
|
text: rawLine.trim(),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
1
|
+
import { join, dirname, basename } from 'node:path';
|
|
2
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
3
3
|
import { pathExists } from '../../shared/fs.js';
|
|
4
4
|
export const VALID_REQUEST_TYPES = [
|
|
5
5
|
'feature',
|
|
@@ -111,6 +111,36 @@ export function getPrerequisitesFor(role, newState, requestType = DEFAULT_REQUES
|
|
|
111
111
|
function resolvePrerequisitePath(prerequisite, requestId) {
|
|
112
112
|
return prerequisite.relativePath.replace('<rid>', requestId);
|
|
113
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Resolve a prerequisite to an on-disk path, tolerating the numbered filename
|
|
116
|
+
* prefix that `request init` writes (e.g. `001-<rid>.md`). When the prerequisite
|
|
117
|
+
* path contains `<rid>`, we accept either the legacy bare `<rid>.md` form or any
|
|
118
|
+
* `NNN-<rid>.md` numbered form — mirroring the matcher in request-artifact-service.
|
|
119
|
+
* Returns the matched absolute path, or null when nothing matches.
|
|
120
|
+
*/
|
|
121
|
+
async function resolvePrerequisiteAbsolutePath(sessionRoot, prerequisite, requestId) {
|
|
122
|
+
const relative = resolvePrerequisitePath(prerequisite, requestId);
|
|
123
|
+
const exact = join(sessionRoot, relative);
|
|
124
|
+
if (await pathExists(exact)) {
|
|
125
|
+
return exact;
|
|
126
|
+
}
|
|
127
|
+
// Only `<rid>`-templated prerequisites can carry a numbered prefix; fixed paths
|
|
128
|
+
// (e.g. rd/tech-doc.md) are matched exactly above.
|
|
129
|
+
if (!prerequisite.relativePath.includes('<rid>')) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const dir = dirname(exact);
|
|
133
|
+
const targetSuffix = `-${basename(exact)}`;
|
|
134
|
+
let entries;
|
|
135
|
+
try {
|
|
136
|
+
entries = await readdir(dir);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const match = entries.find((name) => /^\d+-/.test(name) && name.endsWith(targetSuffix));
|
|
142
|
+
return match ? join(dir, match) : null;
|
|
143
|
+
}
|
|
114
144
|
export async function checkPrerequisites(options) {
|
|
115
145
|
const requirements = getPrerequisitesFor(options.role, options.newState, options.requestType);
|
|
116
146
|
if (requirements.length === 0) {
|
|
@@ -120,8 +150,8 @@ export async function checkPrerequisites(options) {
|
|
|
120
150
|
const missing = [];
|
|
121
151
|
for (const prerequisite of requirements) {
|
|
122
152
|
const relative = resolvePrerequisitePath(prerequisite, options.requestId);
|
|
123
|
-
const absolute =
|
|
124
|
-
if (
|
|
153
|
+
const absolute = await resolvePrerequisiteAbsolutePath(sessionRoot, prerequisite, options.requestId);
|
|
154
|
+
if (absolute === null) {
|
|
125
155
|
missing.push({ path: relative, description: prerequisite.description });
|
|
126
156
|
continue;
|
|
127
157
|
}
|
|
@@ -24,5 +24,6 @@ export type DoctorOptions = {
|
|
|
24
24
|
codegraphProbe?: () => CodegraphCapabilityProbe;
|
|
25
25
|
skillPresenceProbe?: () => SkillPresence | null;
|
|
26
26
|
skillPresenceFreshnessThresholdMs?: number;
|
|
27
|
+
statusLineInstalledProbe?: () => boolean;
|
|
27
28
|
};
|
|
28
29
|
export declare function runDoctor(options?: DoctorOptions): Promise<DoctorReport>;
|
|
@@ -8,6 +8,8 @@ import { requiredSchemaFiles, requiredSkillNames, schemasDir } from '../../share
|
|
|
8
8
|
import { getErrorMessage } from '../../shared/result.js';
|
|
9
9
|
import { loadSkillRegistry } from '../skills/skill-registry.js';
|
|
10
10
|
import { getSkillPresence } from '../skills/skill-presence-service.js';
|
|
11
|
+
import { planStatusLineInstall } from '../skills/statusline-settings-service.js';
|
|
12
|
+
import { findProjectRoot } from '../config/config-safety.js';
|
|
11
13
|
const CODEGRAPH_EXPECTED_VERSION = '0.7.10';
|
|
12
14
|
const SKILL_PRESENCE_FRESHNESS_THRESHOLD_MS = 24 * 60 * 60 * 1000;
|
|
13
15
|
function defaultCodegraphProbe() {
|
|
@@ -22,6 +24,17 @@ function defaultCodegraphProbe() {
|
|
|
22
24
|
binaryExists: existsSync(binaryPath)
|
|
23
25
|
};
|
|
24
26
|
}
|
|
27
|
+
function defaultStatusLineInstalledProbe() {
|
|
28
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
29
|
+
if (projectRoot === null)
|
|
30
|
+
return false;
|
|
31
|
+
try {
|
|
32
|
+
return planStatusLineInstall('project', projectRoot).alreadyInstalled;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
25
38
|
const DESTRUCTIVE_APPLY_PATTERNS = [
|
|
26
39
|
/peaks\s+memory\s+sync[^\n]*--apply/,
|
|
27
40
|
/peaks\s+memory\s+extract[^\n]*--apply/,
|
|
@@ -190,6 +203,33 @@ export async function runDoctor(options = {}) {
|
|
|
190
203
|
}
|
|
191
204
|
}
|
|
192
205
|
}
|
|
206
|
+
// Discoverability nudge: when a skill is actively orchestrating but the
|
|
207
|
+
// out-of-band statusLine isn't installed, the user has no terminal-level
|
|
208
|
+
// signal that Peaks is in control. Suggest installing it (non-failing).
|
|
209
|
+
const statusLineProbe = options.statusLineInstalledProbe ?? defaultStatusLineInstalledProbe;
|
|
210
|
+
let statusLineInstalled = false;
|
|
211
|
+
try {
|
|
212
|
+
statusLineInstalled = statusLineProbe();
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
statusLineInstalled = false;
|
|
216
|
+
}
|
|
217
|
+
if (presence !== null && !statusLineInstalled) {
|
|
218
|
+
checks.push({
|
|
219
|
+
id: 'statusline:install',
|
|
220
|
+
ok: true,
|
|
221
|
+
message: 'A Peaks skill is active but the statusLine is not installed; run `peaks statusline install` so the active skill shows in the terminal status bar'
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
checks.push({
|
|
226
|
+
id: 'statusline:install',
|
|
227
|
+
ok: true,
|
|
228
|
+
message: statusLineInstalled
|
|
229
|
+
? 'Peaks statusLine is installed'
|
|
230
|
+
: 'Peaks statusLine not installed (no active skill; install optional)'
|
|
231
|
+
});
|
|
232
|
+
}
|
|
193
233
|
const probe = options.codegraphProbe ?? defaultCodegraphProbe;
|
|
194
234
|
try {
|
|
195
235
|
const result = probe();
|
|
@@ -25,6 +25,16 @@ function classifyFile(filePath) {
|
|
|
25
25
|
return 'source';
|
|
26
26
|
return 'unknown';
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Peaks' own artifact workspace. Changes here (PRD/RD/QA markdown, session
|
|
30
|
+
* state) are never the "code change" a request type describes, so they must be
|
|
31
|
+
* excluded from the diff — otherwise a PRD-planning-phase handoff that only
|
|
32
|
+
* wrote `.peaks/**` markdown would be misclassified as a docs change.
|
|
33
|
+
*/
|
|
34
|
+
function isArtifactWorkspaceFile(filePath) {
|
|
35
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
36
|
+
return normalized === '.peaks' || normalized.startsWith('.peaks/');
|
|
37
|
+
}
|
|
28
38
|
function tryGitDiffFiles(projectRoot, baseRef) {
|
|
29
39
|
try {
|
|
30
40
|
// Combine: tracked changes vs baseRef + untracked files. Use porcelain status for untracked too.
|
|
@@ -32,7 +42,7 @@ function tryGitDiffFiles(projectRoot, baseRef) {
|
|
|
32
42
|
const tracked = trackedRaw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
33
43
|
const untrackedRaw = execFileSync('git', ['-C', projectRoot, 'ls-files', '--others', '--exclude-standard'], { encoding: 'utf8' });
|
|
34
44
|
const untracked = untrackedRaw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
35
|
-
const merged = Array.from(new Set([...tracked, ...untracked]));
|
|
45
|
+
const merged = Array.from(new Set([...tracked, ...untracked])).filter((file) => !isArtifactWorkspaceFile(file));
|
|
36
46
|
return { ok: true, files: merged };
|
|
37
47
|
}
|
|
38
48
|
catch {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { StatusLineModel } from './skill-statusline-service.js';
|
|
2
|
+
/**
|
|
3
|
+
* Render the status line. The output is plain text with simple status glyphs so
|
|
4
|
+
* it stays readable in any terminal; Claude Code applies its own styling.
|
|
5
|
+
*/
|
|
6
|
+
export declare function renderStatusLine(model: StatusLineModel): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
/**
|
|
3
|
+
* Pure formatting layer for the Peaks statusLine. Takes the read-only status
|
|
4
|
+
* model and produces the single line Claude Code paints at the bottom of the
|
|
5
|
+
* terminal. Kept separate from the reader so formatting can be tested without
|
|
6
|
+
* touching the filesystem.
|
|
7
|
+
*/
|
|
8
|
+
const BRAND = '⛰ Peaks';
|
|
9
|
+
function formatAge(ageMs) {
|
|
10
|
+
if (ageMs === null)
|
|
11
|
+
return '';
|
|
12
|
+
const hours = Math.round(ageMs / (60 * 60 * 1000));
|
|
13
|
+
if (hours >= 1)
|
|
14
|
+
return `stale ${hours}h`;
|
|
15
|
+
const minutes = Math.max(1, Math.round(ageMs / (60 * 1000)));
|
|
16
|
+
return `stale ${minutes}m`;
|
|
17
|
+
}
|
|
18
|
+
function rootLabel(projectRoot) {
|
|
19
|
+
if (!projectRoot)
|
|
20
|
+
return '';
|
|
21
|
+
return basename(projectRoot);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Render the status line. The output is plain text with simple status glyphs so
|
|
25
|
+
* it stays readable in any terminal; Claude Code applies its own styling.
|
|
26
|
+
*/
|
|
27
|
+
export function renderStatusLine(model) {
|
|
28
|
+
const root = rootLabel(model.projectRoot);
|
|
29
|
+
const rootSuffix = root ? ` · ${root}` : '';
|
|
30
|
+
switch (model.state) {
|
|
31
|
+
case 'active': {
|
|
32
|
+
const presence = model.presence;
|
|
33
|
+
if (!presence)
|
|
34
|
+
return `${BRAND} ○ idle${rootSuffix}`;
|
|
35
|
+
const parts = [presence.skill];
|
|
36
|
+
if (presence.mode)
|
|
37
|
+
parts.push(presence.mode);
|
|
38
|
+
if (presence.gate)
|
|
39
|
+
parts.push(`gate:${presence.gate}`);
|
|
40
|
+
return `${BRAND} ● ${parts.join(' · ')}${rootSuffix}`;
|
|
41
|
+
}
|
|
42
|
+
case 'stale': {
|
|
43
|
+
const presence = model.presence;
|
|
44
|
+
const skill = presence?.skill ?? 'unknown';
|
|
45
|
+
const age = formatAge(model.ageMs);
|
|
46
|
+
const ageSuffix = age ? ` · ${age}` : '';
|
|
47
|
+
return `${BRAND} ⚠ ${skill}${ageSuffix}${rootSuffix}`;
|
|
48
|
+
}
|
|
49
|
+
case 'invalid-presence':
|
|
50
|
+
return `${BRAND} ⚠ presence file unreadable${rootSuffix}`;
|
|
51
|
+
case 'idle':
|
|
52
|
+
default:
|
|
53
|
+
return `${BRAND} ○ idle${rootSuffix}`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type StatusLineStdin = {
|
|
2
|
+
workspace?: {
|
|
3
|
+
current_dir?: string;
|
|
4
|
+
project_dir?: string;
|
|
5
|
+
};
|
|
6
|
+
cwd?: string;
|
|
7
|
+
};
|
|
8
|
+
export type StatusLineState = 'active' | 'idle' | 'stale' | 'invalid-presence';
|
|
9
|
+
export type StatusLinePresence = {
|
|
10
|
+
skill: string;
|
|
11
|
+
mode?: string;
|
|
12
|
+
gate?: string;
|
|
13
|
+
setAt?: string;
|
|
14
|
+
};
|
|
15
|
+
export type StatusLineModel = {
|
|
16
|
+
state: StatusLineState;
|
|
17
|
+
projectRoot: string | null;
|
|
18
|
+
presence: StatusLinePresence | null;
|
|
19
|
+
ageMs: number | null;
|
|
20
|
+
};
|
|
21
|
+
export declare function parseStatusLineStdin(raw: string): StatusLineStdin | null;
|
|
22
|
+
export declare function buildStatusLineModel(stdin: StatusLineStdin | null, nowMs: number): StatusLineModel;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { findProjectRoot } from '../config/config-safety.js';
|
|
4
|
+
/**
|
|
5
|
+
* Out-of-band Peaks skill status renderer for the Claude Code statusLine.
|
|
6
|
+
*
|
|
7
|
+
* Claude Code invokes the configured statusLine command on every turn and pipes
|
|
8
|
+
* a JSON session payload on stdin. This renderer reads the durable presence file
|
|
9
|
+
* (.peaks/.active-skill.json) and prints a single line that Claude Code paints at
|
|
10
|
+
* the bottom of the terminal. Because it is rendered by the harness — not emitted
|
|
11
|
+
* as LLM tokens — the signal cannot be forgotten by the model, cannot be confused
|
|
12
|
+
* with normal output, and survives context compaction.
|
|
13
|
+
*
|
|
14
|
+
* This module is intentionally READ-ONLY. Unlike getSkillPresence in
|
|
15
|
+
* skill-presence-service.ts, it never deletes or rewrites the presence file:
|
|
16
|
+
* the statusLine runs on every turn and must have zero side effects.
|
|
17
|
+
*/
|
|
18
|
+
const PRESENCE_FILE = '.peaks/.active-skill.json';
|
|
19
|
+
const STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
|
|
20
|
+
function resolveCwdFromStdin(stdin) {
|
|
21
|
+
const fromWorkspace = stdin?.workspace?.current_dir ?? stdin?.workspace?.project_dir;
|
|
22
|
+
if (typeof fromWorkspace === 'string' && fromWorkspace.length > 0) {
|
|
23
|
+
return resolve(fromWorkspace);
|
|
24
|
+
}
|
|
25
|
+
if (typeof stdin?.cwd === 'string' && stdin.cwd.length > 0) {
|
|
26
|
+
return resolve(stdin.cwd);
|
|
27
|
+
}
|
|
28
|
+
return process.cwd();
|
|
29
|
+
}
|
|
30
|
+
export function parseStatusLineStdin(raw) {
|
|
31
|
+
const trimmed = raw.trim();
|
|
32
|
+
if (trimmed.length === 0)
|
|
33
|
+
return null;
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(trimmed);
|
|
36
|
+
if (parsed && typeof parsed === 'object') {
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read the presence file without any side effects. Returns null when the file is
|
|
47
|
+
* absent (idle) and a sentinel object for malformed content (invalid-presence).
|
|
48
|
+
*/
|
|
49
|
+
function readPresenceReadOnly(projectRoot) {
|
|
50
|
+
const presencePath = resolve(projectRoot, PRESENCE_FILE);
|
|
51
|
+
if (!existsSync(presencePath)) {
|
|
52
|
+
return { presence: null, invalid: false };
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(readFileSync(presencePath, 'utf8'));
|
|
56
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
57
|
+
return { presence: null, invalid: true };
|
|
58
|
+
}
|
|
59
|
+
const candidate = parsed;
|
|
60
|
+
if (typeof candidate.skill !== 'string' || candidate.skill.length === 0) {
|
|
61
|
+
return { presence: null, invalid: true };
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
presence: {
|
|
65
|
+
skill: candidate.skill,
|
|
66
|
+
...(typeof candidate.mode === 'string' ? { mode: candidate.mode } : {}),
|
|
67
|
+
...(typeof candidate.gate === 'string' ? { gate: candidate.gate } : {}),
|
|
68
|
+
...(typeof candidate.setAt === 'string' ? { setAt: candidate.setAt } : {})
|
|
69
|
+
},
|
|
70
|
+
invalid: false
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return { presence: null, invalid: true };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function buildStatusLineModel(stdin, nowMs) {
|
|
78
|
+
const cwd = resolveCwdFromStdin(stdin);
|
|
79
|
+
const projectRoot = findProjectRoot(cwd);
|
|
80
|
+
if (projectRoot === null) {
|
|
81
|
+
return { state: 'idle', projectRoot: null, presence: null, ageMs: null };
|
|
82
|
+
}
|
|
83
|
+
const { presence, invalid } = readPresenceReadOnly(projectRoot);
|
|
84
|
+
if (invalid) {
|
|
85
|
+
return { state: 'invalid-presence', projectRoot, presence: null, ageMs: null };
|
|
86
|
+
}
|
|
87
|
+
if (presence === null) {
|
|
88
|
+
return { state: 'idle', projectRoot, presence: null, ageMs: null };
|
|
89
|
+
}
|
|
90
|
+
const setAtMs = presence.setAt ? Date.parse(presence.setAt) : Number.NaN;
|
|
91
|
+
const ageMs = Number.isNaN(setAtMs) ? null : nowMs - setAtMs;
|
|
92
|
+
const state = ageMs !== null && ageMs > STALE_THRESHOLD_MS ? 'stale' : 'active';
|
|
93
|
+
return { state, projectRoot, presence, ageMs };
|
|
94
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installs (and removes) the Peaks statusLine entry in a Claude Code
|
|
3
|
+
* settings.json. The statusLine renders `peaks statusline` on every turn, giving
|
|
4
|
+
* users an out-of-band, harness-painted signal of which Peaks skill is active —
|
|
5
|
+
* independent of LLM tokens and immune to context compaction.
|
|
6
|
+
*
|
|
7
|
+
* Writes preserve all other settings keys, reject symlinked targets, and use an
|
|
8
|
+
* atomic rename so a partial write can never corrupt an existing settings file.
|
|
9
|
+
*/
|
|
10
|
+
export type StatusLineScope = 'project' | 'global';
|
|
11
|
+
export type StatusLineSettingsPlan = {
|
|
12
|
+
scope: StatusLineScope;
|
|
13
|
+
settingsPath: string;
|
|
14
|
+
exists: boolean;
|
|
15
|
+
alreadyInstalled: boolean;
|
|
16
|
+
conflict: boolean;
|
|
17
|
+
conflictCommand: string | null;
|
|
18
|
+
desiredCommand: string;
|
|
19
|
+
};
|
|
20
|
+
export type StatusLineSettingsResult = StatusLineSettingsPlan & {
|
|
21
|
+
applied: boolean;
|
|
22
|
+
};
|
|
23
|
+
export declare const STATUSLINE_COMMAND = "peaks statusline";
|
|
24
|
+
export declare function planStatusLineInstall(scope: StatusLineScope, projectRoot?: string): StatusLineSettingsPlan;
|
|
25
|
+
export declare function applyStatusLineInstall(scope: StatusLineScope, projectRoot?: string, options?: {
|
|
26
|
+
force?: boolean;
|
|
27
|
+
}): StatusLineSettingsResult;
|
|
28
|
+
export declare function removeStatusLineInstall(scope: StatusLineScope, projectRoot?: string): {
|
|
29
|
+
scope: StatusLineScope;
|
|
30
|
+
settingsPath: string;
|
|
31
|
+
removed: boolean;
|
|
32
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
export const STATUSLINE_COMMAND = 'peaks statusline';
|
|
6
|
+
function isInsidePath(childPath, parentPath) {
|
|
7
|
+
const rel = relative(parentPath, childPath);
|
|
8
|
+
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
9
|
+
}
|
|
10
|
+
function resolveSettingsRoot(scope, projectRoot) {
|
|
11
|
+
if (scope === 'global')
|
|
12
|
+
return resolve(homedir());
|
|
13
|
+
if (!projectRoot) {
|
|
14
|
+
throw new Error('Project scope requires a project root');
|
|
15
|
+
}
|
|
16
|
+
return resolve(projectRoot);
|
|
17
|
+
}
|
|
18
|
+
function resolveSettingsPath(scope, projectRoot) {
|
|
19
|
+
const root = resolveSettingsRoot(scope, projectRoot);
|
|
20
|
+
return join(root, '.claude', 'settings.json');
|
|
21
|
+
}
|
|
22
|
+
/** Reject symlinked .claude dir or settings file to prevent escape. */
|
|
23
|
+
function assertSafeSettingsPath(scope, root, settingsPath) {
|
|
24
|
+
const claudeDir = join(root, '.claude');
|
|
25
|
+
if (existsSync(claudeDir) && lstatSync(claudeDir).isSymbolicLink()) {
|
|
26
|
+
throw new Error('.claude directory must not be a symlink');
|
|
27
|
+
}
|
|
28
|
+
if (existsSync(settingsPath)) {
|
|
29
|
+
if (lstatSync(settingsPath).isSymbolicLink()) {
|
|
30
|
+
throw new Error('settings.json must not be a symlink');
|
|
31
|
+
}
|
|
32
|
+
const realRoot = realpathSync(root);
|
|
33
|
+
if (!isInsidePath(realpathSync(settingsPath), realRoot)) {
|
|
34
|
+
throw new Error(`settings.json must stay inside the ${scope} root`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function readSettings(settingsPath) {
|
|
39
|
+
if (!existsSync(settingsPath))
|
|
40
|
+
return {};
|
|
41
|
+
const fd = openSync(settingsPath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
42
|
+
try {
|
|
43
|
+
const raw = readFileSync(fd, 'utf8').trim();
|
|
44
|
+
if (raw.length === 0)
|
|
45
|
+
return {};
|
|
46
|
+
const parsed = JSON.parse(raw);
|
|
47
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
48
|
+
throw new Error('settings.json must contain a JSON object');
|
|
49
|
+
}
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
closeSync(fd);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function extractExistingCommand(settings) {
|
|
57
|
+
const statusLine = settings.statusLine;
|
|
58
|
+
if (statusLine && typeof statusLine === 'object' && !Array.isArray(statusLine)) {
|
|
59
|
+
const command = statusLine.command;
|
|
60
|
+
if (typeof command === 'string')
|
|
61
|
+
return command;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
function buildPlan(scope, settingsPath, settings, exists) {
|
|
66
|
+
const existingCommand = extractExistingCommand(settings);
|
|
67
|
+
const alreadyInstalled = existingCommand !== null && existingCommand.includes(STATUSLINE_COMMAND);
|
|
68
|
+
const conflict = existingCommand !== null && !alreadyInstalled;
|
|
69
|
+
return {
|
|
70
|
+
scope,
|
|
71
|
+
settingsPath,
|
|
72
|
+
exists,
|
|
73
|
+
alreadyInstalled,
|
|
74
|
+
conflict,
|
|
75
|
+
conflictCommand: conflict ? existingCommand : null,
|
|
76
|
+
desiredCommand: STATUSLINE_COMMAND
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function planStatusLineInstall(scope, projectRoot) {
|
|
80
|
+
const root = resolveSettingsRoot(scope, projectRoot);
|
|
81
|
+
const settingsPath = resolveSettingsPath(scope, projectRoot);
|
|
82
|
+
assertSafeSettingsPath(scope, root, settingsPath);
|
|
83
|
+
const exists = existsSync(settingsPath);
|
|
84
|
+
const settings = readSettings(settingsPath);
|
|
85
|
+
return buildPlan(scope, settingsPath, settings, exists);
|
|
86
|
+
}
|
|
87
|
+
function atomicWriteJson(settingsPath, settings) {
|
|
88
|
+
const dir = dirname(settingsPath);
|
|
89
|
+
mkdirSync(dir, { recursive: true });
|
|
90
|
+
const tempPath = join(dir, `.settings.${randomUUID()}.tmp`);
|
|
91
|
+
const fd = openSync(tempPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
92
|
+
try {
|
|
93
|
+
writeFileSync(fd, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
closeSync(fd);
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
renameSync(tempPath, settingsPath);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
try {
|
|
103
|
+
unlinkSync(tempPath);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// best effort cleanup
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function applyStatusLineInstall(scope, projectRoot, options = {}) {
|
|
112
|
+
const root = resolveSettingsRoot(scope, projectRoot);
|
|
113
|
+
const settingsPath = resolveSettingsPath(scope, projectRoot);
|
|
114
|
+
assertSafeSettingsPath(scope, root, settingsPath);
|
|
115
|
+
const exists = existsSync(settingsPath);
|
|
116
|
+
const settings = readSettings(settingsPath);
|
|
117
|
+
const plan = buildPlan(scope, settingsPath, settings, exists);
|
|
118
|
+
if (plan.alreadyInstalled) {
|
|
119
|
+
return { ...plan, applied: false };
|
|
120
|
+
}
|
|
121
|
+
if (plan.conflict && !options.force) {
|
|
122
|
+
return { ...plan, applied: false };
|
|
123
|
+
}
|
|
124
|
+
const entry = { type: 'command', command: STATUSLINE_COMMAND, padding: 0 };
|
|
125
|
+
const nextSettings = { ...settings, statusLine: entry };
|
|
126
|
+
atomicWriteJson(settingsPath, nextSettings);
|
|
127
|
+
return { ...plan, applied: true };
|
|
128
|
+
}
|
|
129
|
+
export function removeStatusLineInstall(scope, projectRoot) {
|
|
130
|
+
const root = resolveSettingsRoot(scope, projectRoot);
|
|
131
|
+
const settingsPath = resolveSettingsPath(scope, projectRoot);
|
|
132
|
+
assertSafeSettingsPath(scope, root, settingsPath);
|
|
133
|
+
if (!existsSync(settingsPath)) {
|
|
134
|
+
return { scope, settingsPath, removed: false };
|
|
135
|
+
}
|
|
136
|
+
const settings = readSettings(settingsPath);
|
|
137
|
+
const existingCommand = extractExistingCommand(settings);
|
|
138
|
+
if (existingCommand === null || !existingCommand.includes(STATUSLINE_COMMAND)) {
|
|
139
|
+
return { scope, settingsPath, removed: false };
|
|
140
|
+
}
|
|
141
|
+
const { statusLine: _removed, ...rest } = settings;
|
|
142
|
+
atomicWriteJson(settingsPath, rest);
|
|
143
|
+
return { scope, settingsPath, removed: true };
|
|
144
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "1.1.
|
|
1
|
+
export declare const CLI_VERSION = "1.1.2";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.1.
|
|
1
|
+
export const CLI_VERSION = "1.1.2";
|
package/package.json
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
"properties": {
|
|
14
14
|
"id": {
|
|
15
15
|
"type": "string",
|
|
16
|
-
"pattern": "^(skill|skill-name|skill-parse|skill-runbook|skill-apply-note|skill-presence|schema|config|doctor-self|capability):[A-Za-z0-9][A-Za-z0-9._-]*$",
|
|
17
|
-
"description": "Stable check id. Known prefixes: skill:<name> (required skill present), skill-name:<dir> (directory matches declared name), skill-parse:<dir> (skill metadata parsed), skill-runbook:<name> (Default runbook section exists), skill-apply-note:<name> (destructive --apply lines carry an authorization/--dry-run note), skill-presence:<topic> (status of .peaks/.active-skill.json — current/freshness), schema:<file> (schema file exists and is valid JSON), config:<scope> (optional config locations), doctor-self:<topic> (doctor validates its own output against this schema), capability:<name> (third-party capability is resolvable at the pinned version)."
|
|
16
|
+
"pattern": "^(skill|skill-name|skill-parse|skill-runbook|skill-apply-note|skill-presence|statusline|schema|config|doctor-self|capability):[A-Za-z0-9][A-Za-z0-9._-]*$",
|
|
17
|
+
"description": "Stable check id. Known prefixes: skill:<name> (required skill present), skill-name:<dir> (directory matches declared name), skill-parse:<dir> (skill metadata parsed), skill-runbook:<name> (Default runbook section exists), skill-apply-note:<name> (destructive --apply lines carry an authorization/--dry-run note), skill-presence:<topic> (status of .peaks/.active-skill.json — current/freshness), statusline:<topic> (whether the out-of-band Claude Code statusLine is installed), schema:<file> (schema file exists and is valid JSON), config:<scope> (optional config locations), doctor-self:<topic> (doctor validates its own output against this schema), capability:<name> (third-party capability is resolvable at the pinned version)."
|
|
18
18
|
},
|
|
19
19
|
"ok": { "type": "boolean" },
|
|
20
20
|
"message": { "type": "string", "minLength": 1 }
|
|
@@ -14,6 +14,13 @@ Before any analysis or tool call, immediately run:
|
|
|
14
14
|
```bash
|
|
15
15
|
peaks skill presence:set peaks-prd --project <repo> --mode <mode> --gate startup
|
|
16
16
|
```
|
|
17
|
+
|
|
18
|
+
On the first presence:set in a project, ensure the out-of-band status bar is installed so the user can see at a glance that Peaks is orchestrating — it renders the active skill in Claude Code's terminal status line, independent of model output:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
peaks statusline install --project <repo> # idempotent; skips if already installed
|
|
22
|
+
```
|
|
23
|
+
|
|
17
24
|
Read persistent project memory via CLI (durable, LLM-authored memories):
|
|
18
25
|
|
|
19
26
|
```bash
|
package/skills/peaks-qa/SKILL.md
CHANGED
|
@@ -14,6 +14,13 @@ Before any analysis or tool call, immediately run:
|
|
|
14
14
|
```bash
|
|
15
15
|
peaks skill presence:set peaks-qa --project <repo> --mode <mode> --gate startup
|
|
16
16
|
```
|
|
17
|
+
|
|
18
|
+
On the first presence:set in a project, ensure the out-of-band status bar is installed so the user can see at a glance that Peaks is orchestrating — it renders the active skill in Claude Code's terminal status line, independent of model output:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
peaks statusline install --project <repo> # idempotent; skips if already installed
|
|
22
|
+
```
|
|
23
|
+
|
|
17
24
|
Read persistent project memory via CLI (durable, LLM-authored memories):
|
|
18
25
|
|
|
19
26
|
```bash
|
package/skills/peaks-rd/SKILL.md
CHANGED
|
@@ -14,6 +14,13 @@ Before any analysis or tool call, immediately run:
|
|
|
14
14
|
```bash
|
|
15
15
|
peaks skill presence:set peaks-rd --project <repo> --mode <mode> --gate startup
|
|
16
16
|
```
|
|
17
|
+
|
|
18
|
+
On the first presence:set in a project, ensure the out-of-band status bar is installed so the user can see at a glance that Peaks is orchestrating — it renders the active skill in Claude Code's terminal status line, independent of model output:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
peaks statusline install --project <repo> # idempotent; skips if already installed
|
|
22
|
+
```
|
|
23
|
+
|
|
17
24
|
Read persistent project memory via CLI (durable, LLM-authored memories):
|
|
18
25
|
|
|
19
26
|
```bash
|
package/skills/peaks-sc/SKILL.md
CHANGED
|
@@ -14,6 +14,13 @@ Before any analysis or tool call, immediately run:
|
|
|
14
14
|
```bash
|
|
15
15
|
peaks skill presence:set peaks-sc --project <repo> --mode <mode> --gate startup
|
|
16
16
|
```
|
|
17
|
+
|
|
18
|
+
On the first presence:set in a project, ensure the out-of-band status bar is installed so the user can see at a glance that Peaks is orchestrating — it renders the active skill in Claude Code's terminal status line, independent of model output:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
peaks statusline install --project <repo> # idempotent; skips if already installed
|
|
22
|
+
```
|
|
23
|
+
|
|
17
24
|
Read persistent project memory via CLI (durable, LLM-authored memories):
|
|
18
25
|
|
|
19
26
|
```bash
|
|
@@ -74,6 +74,12 @@ Only after the mode is known (user selected or explicitly named), run:
|
|
|
74
74
|
peaks skill presence:set peaks-solo --project <repo> --mode <mode-value> --gate startup
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
On the first presence:set in a project, ensure the out-of-band status bar is installed so the user can see at a glance that Peaks is orchestrating — it renders the active skill in Claude Code's terminal status line, independent of model output:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
peaks statusline install --project <repo> # idempotent; skips if already installed
|
|
81
|
+
```
|
|
82
|
+
|
|
77
83
|
Then display the compact status header: `Peaks-Cli Skill: peaks-solo | Peaks-Cli Gate: startup | Next: <one short action>`. Display this header on EVERY turn while the skill is active.
|
|
78
84
|
|
|
79
85
|
Update with `peaks skill presence:set peaks-solo --project <repo> --mode <mode> --gate <gate>` when gates change. The presence file persists across the full workflow lifecycle — do NOT clear it at workflow end.
|
|
@@ -14,6 +14,13 @@ Before any analysis or tool call, immediately run:
|
|
|
14
14
|
```bash
|
|
15
15
|
peaks skill presence:set peaks-txt --project <repo> --mode <mode> --gate startup
|
|
16
16
|
```
|
|
17
|
+
|
|
18
|
+
On the first presence:set in a project, ensure the out-of-band status bar is installed so the user can see at a glance that Peaks is orchestrating — it renders the active skill in Claude Code's terminal status line, independent of model output:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
peaks statusline install --project <repo> # idempotent; skips if already installed
|
|
22
|
+
```
|
|
23
|
+
|
|
17
24
|
Read persistent project memory via CLI (durable, LLM-authored memories):
|
|
18
25
|
|
|
19
26
|
```bash
|
package/skills/peaks-ui/SKILL.md
CHANGED
|
@@ -14,6 +14,13 @@ Before any analysis or tool call, immediately run:
|
|
|
14
14
|
```bash
|
|
15
15
|
peaks skill presence:set peaks-ui --project <repo> --mode <mode> --gate startup
|
|
16
16
|
```
|
|
17
|
+
|
|
18
|
+
On the first presence:set in a project, ensure the out-of-band status bar is installed so the user can see at a glance that Peaks is orchestrating — it renders the active skill in Claude Code's terminal status line, independent of model output:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
peaks statusline install --project <repo> # idempotent; skips if already installed
|
|
22
|
+
```
|
|
23
|
+
|
|
17
24
|
Read persistent project memory via CLI (durable, LLM-authored memories):
|
|
18
25
|
|
|
19
26
|
```bash
|