peaks-cli 1.0.25 → 1.0.27
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/core-artifact-commands.js +5 -4
- package/dist/src/cli/commands/workflow-commands.js +5 -2
- package/dist/src/services/config/config-safety.js +14 -4
- package/dist/src/services/skills/skill-presence-service.d.ts +1 -0
- package/dist/src/services/skills/skill-presence-service.js +36 -1
- package/dist/src/shared/change-id.js +4 -2
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +2 -2
- package/skills/peaks-solo/SKILL.md +14 -2
package/bin/peaks.js
CHANGED
|
File without changes
|
|
@@ -9,6 +9,7 @@ import { listSkills } from '../../services/skills/skill-registry.js';
|
|
|
9
9
|
import { inspectSkillRunbook } from '../../services/skills/skill-runbook-service.js';
|
|
10
10
|
import { setSkillPresence, clearSkillPresence, getSkillPresence, isSkillPresenceMode, touchSkillHeartbeat } from '../../services/skills/skill-presence-service.js';
|
|
11
11
|
import { ensureSession, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas } from '../../services/session/session-manager.js';
|
|
12
|
+
import { findProjectRoot } from '../../services/config/config-safety.js';
|
|
12
13
|
import { fail, ok } from '../../shared/result.js';
|
|
13
14
|
import { addJsonOption, failUnsupportedNonDryRun, getErrorMessage, isArtifactProvider, isArtifactSetupStep, printResult } from '../cli-helpers.js';
|
|
14
15
|
export function registerCoreAndArtifactCommands(program, io) {
|
|
@@ -80,7 +81,7 @@ export function registerCoreAndArtifactCommands(program, io) {
|
|
|
80
81
|
}
|
|
81
82
|
const presence = setSkillPresence(name, options.mode, options.gate);
|
|
82
83
|
// Also update session metadata so session dirs self-document
|
|
83
|
-
const projectRoot = process.cwd();
|
|
84
|
+
const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
|
|
84
85
|
const sessionId = await ensureSession(projectRoot);
|
|
85
86
|
setSessionMeta(projectRoot, sessionId, {
|
|
86
87
|
skill: name,
|
|
@@ -129,14 +130,14 @@ export function registerCoreAndArtifactCommands(program, io) {
|
|
|
129
130
|
addJsonOption(session
|
|
130
131
|
.command('list')
|
|
131
132
|
.description('List all session directories with titles and metadata')).action((options) => {
|
|
132
|
-
const projectRoot = process.cwd();
|
|
133
|
+
const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
|
|
133
134
|
const metas = listSessionMetas(projectRoot);
|
|
134
135
|
printResult(io, ok('session.list', { sessions: metas, total: metas.length }), options.json);
|
|
135
136
|
});
|
|
136
137
|
addJsonOption(session
|
|
137
138
|
.command('info <sessionId>')
|
|
138
139
|
.description('Show full metadata for a session directory')).action((sessionId, options) => {
|
|
139
|
-
const projectRoot = process.cwd();
|
|
140
|
+
const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
|
|
140
141
|
const meta = getSessionMeta(projectRoot, sessionId);
|
|
141
142
|
if (meta === null) {
|
|
142
143
|
printResult(io, fail('session.info', 'SESSION_NOT_FOUND', `Session "${sessionId}" not found or has no metadata`, { sessionId }, ['Use `peaks session list` to see available sessions']), options.json);
|
|
@@ -148,7 +149,7 @@ export function registerCoreAndArtifactCommands(program, io) {
|
|
|
148
149
|
addJsonOption(session
|
|
149
150
|
.command('title <sessionId> <title>')
|
|
150
151
|
.description('Set a human-readable title for a session directory')).action((sessionId, title, options) => {
|
|
151
|
-
const projectRoot = process.cwd();
|
|
152
|
+
const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
|
|
152
153
|
try {
|
|
153
154
|
const meta = setSessionTitle(projectRoot, sessionId, title);
|
|
154
155
|
printResult(io, ok('session.title', meta), options.json);
|
|
@@ -10,12 +10,14 @@ import { validateChangeIdOrThrow } from '../../shared/change-id.js';
|
|
|
10
10
|
import { getEconomyAwareExecutionModelId } from '../../services/config/model-routing.js';
|
|
11
11
|
import { getLocalArtifactPath } from '../../services/artifacts/workspace-service.js';
|
|
12
12
|
import { getSessionId } from '../../services/session/session-manager.js';
|
|
13
|
+
import { findProjectRoot } from '../../services/config/config-safety.js';
|
|
13
14
|
import { verifyPipeline } from '../../services/workflow/pipeline-verify-service.js';
|
|
14
15
|
import { fail, ok } from '../../shared/result.js';
|
|
15
16
|
import { addJsonOption, failUnsupportedNonDryRun, getErrorMessage, isRecommendationWorkflow, printResult } from '../cli-helpers.js';
|
|
16
17
|
function getCurrentWorkspaceContext() {
|
|
17
18
|
try {
|
|
18
|
-
const
|
|
19
|
+
const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
|
|
20
|
+
const sessionId = getSessionId(projectRoot);
|
|
19
21
|
return sessionId ? { sessionId, sessionDir: `.peaks/${sessionId}` } : {};
|
|
20
22
|
}
|
|
21
23
|
catch {
|
|
@@ -24,7 +26,8 @@ function getCurrentWorkspaceContext() {
|
|
|
24
26
|
}
|
|
25
27
|
function getWorkflowWorkspaceContext() {
|
|
26
28
|
try {
|
|
27
|
-
const
|
|
29
|
+
const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
|
|
30
|
+
const workspace = getWorkspaceConfigForPath(projectRoot);
|
|
28
31
|
if (!workspace)
|
|
29
32
|
return {};
|
|
30
33
|
return { workspace, artifactWorkspacePath: getLocalArtifactPath(workspace) };
|
|
@@ -50,34 +50,44 @@ export function findProjectRoot(startPath) {
|
|
|
50
50
|
const homeBoundaryPaths = getHomeBoundaryPaths();
|
|
51
51
|
let current = resolve(startPath);
|
|
52
52
|
let parent = dirname(current);
|
|
53
|
+
let pkgRoot = null;
|
|
53
54
|
while (current !== parent && !homeBoundaryPaths.has(normalizeBoundaryPath(current))) {
|
|
54
55
|
if (existsSync(resolve(current, '.peaks', 'config.json')) && isSafeProjectConfigMarker(current)) {
|
|
55
56
|
return current;
|
|
56
57
|
}
|
|
57
|
-
|
|
58
|
+
// .git is the definitive project root — return immediately
|
|
59
|
+
if (existsSync(resolve(current, '.git'))) {
|
|
58
60
|
return current;
|
|
59
61
|
}
|
|
62
|
+
// package.json alone is ambiguous in monorepos — keep walking up for .git
|
|
63
|
+
if (pkgRoot === null && existsSync(resolve(current, 'package.json'))) {
|
|
64
|
+
pkgRoot = current;
|
|
65
|
+
}
|
|
60
66
|
parent = current;
|
|
61
67
|
current = dirname(parent);
|
|
62
68
|
}
|
|
63
|
-
return
|
|
69
|
+
return pkgRoot;
|
|
64
70
|
}
|
|
65
71
|
export function resolveProjectRootForConfig(startPath) {
|
|
66
72
|
const start = resolve(startPath);
|
|
67
73
|
const homeBoundaryPaths = getHomeBoundaryPaths();
|
|
68
74
|
let current = start;
|
|
69
75
|
let parent = dirname(current);
|
|
76
|
+
let pkgRoot = null;
|
|
70
77
|
while (current !== parent && !homeBoundaryPaths.has(normalizeBoundaryPath(current))) {
|
|
71
78
|
if (existsSync(resolve(current, '.peaks', 'config.json')) && isSafeProjectConfigMarker(current)) {
|
|
72
79
|
return current;
|
|
73
80
|
}
|
|
74
|
-
if (existsSync(resolve(current, '
|
|
81
|
+
if (existsSync(resolve(current, '.git'))) {
|
|
75
82
|
return current;
|
|
76
83
|
}
|
|
84
|
+
if (pkgRoot === null && existsSync(resolve(current, 'package.json'))) {
|
|
85
|
+
pkgRoot = current;
|
|
86
|
+
}
|
|
77
87
|
parent = current;
|
|
78
88
|
current = dirname(parent);
|
|
79
89
|
}
|
|
80
|
-
return start;
|
|
90
|
+
return pkgRoot ?? start;
|
|
81
91
|
}
|
|
82
92
|
export function getProjectConfigPath(projectRoot) {
|
|
83
93
|
if (!projectRoot)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { findProjectRoot } from '../config/config-safety.js';
|
|
3
4
|
export const VALID_SKILL_PRESENCE_MODES = [
|
|
4
5
|
'full-auto',
|
|
5
6
|
'assisted',
|
|
@@ -10,19 +11,39 @@ export function isSkillPresenceMode(value) {
|
|
|
10
11
|
return VALID_SKILL_PRESENCE_MODES.includes(value);
|
|
11
12
|
}
|
|
12
13
|
const PRESENCE_FILE = '.peaks/.active-skill.json';
|
|
14
|
+
const SESSION_FILE = '.peaks/.session.json';
|
|
15
|
+
function resolveProjectRoot() {
|
|
16
|
+
return findProjectRoot(process.cwd()) ?? process.cwd();
|
|
17
|
+
}
|
|
13
18
|
function resolvePresencePath() {
|
|
14
|
-
return resolve(
|
|
19
|
+
return resolve(resolveProjectRoot(), PRESENCE_FILE);
|
|
20
|
+
}
|
|
21
|
+
function getCurrentSessionId() {
|
|
22
|
+
const sessionPath = resolve(resolveProjectRoot(), SESSION_FILE);
|
|
23
|
+
if (!existsSync(sessionPath))
|
|
24
|
+
return null;
|
|
25
|
+
try {
|
|
26
|
+
const data = JSON.parse(readFileSync(sessionPath, 'utf8'));
|
|
27
|
+
return typeof data.sessionId === 'string' && data.sessionId.length > 0
|
|
28
|
+
? data.sessionId
|
|
29
|
+
: null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
15
34
|
}
|
|
16
35
|
export function exportSkillPresence() {
|
|
17
36
|
return resolvePresencePath();
|
|
18
37
|
}
|
|
19
38
|
export function setSkillPresence(skill, mode, gate) {
|
|
20
39
|
const validatedMode = mode && isSkillPresenceMode(mode) ? mode : undefined;
|
|
40
|
+
const sessionId = getCurrentSessionId();
|
|
21
41
|
const now = new Date().toISOString();
|
|
22
42
|
const presence = {
|
|
23
43
|
skill,
|
|
24
44
|
...(validatedMode ? { mode: validatedMode } : {}),
|
|
25
45
|
...(gate ? { gate } : {}),
|
|
46
|
+
...(sessionId ? { sessionId } : {}),
|
|
26
47
|
setAt: now,
|
|
27
48
|
lastHeartbeat: now
|
|
28
49
|
};
|
|
@@ -45,6 +66,13 @@ export function getSkillPresence() {
|
|
|
45
66
|
if (typeof parsed?.skill !== 'string' || parsed.skill.length === 0) {
|
|
46
67
|
return null;
|
|
47
68
|
}
|
|
69
|
+
if (typeof parsed.sessionId === 'string' && parsed.sessionId.length > 0) {
|
|
70
|
+
const currentSessionId = getCurrentSessionId();
|
|
71
|
+
if (currentSessionId && parsed.sessionId !== currentSessionId) {
|
|
72
|
+
unlinkSync(presencePath);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
48
76
|
return parsed;
|
|
49
77
|
}
|
|
50
78
|
catch {
|
|
@@ -62,6 +90,13 @@ export function touchSkillHeartbeat() {
|
|
|
62
90
|
if (typeof parsed?.skill !== 'string' || parsed.skill.length === 0) {
|
|
63
91
|
return null;
|
|
64
92
|
}
|
|
93
|
+
if (typeof parsed.sessionId === 'string' && parsed.sessionId.length > 0) {
|
|
94
|
+
const currentSessionId = getCurrentSessionId();
|
|
95
|
+
if (currentSessionId && parsed.sessionId !== currentSessionId) {
|
|
96
|
+
unlinkSync(presencePath);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
65
100
|
parsed.lastHeartbeat = new Date().toISOString();
|
|
66
101
|
writeFileSync(presencePath, JSON.stringify(parsed, null, 2), 'utf8');
|
|
67
102
|
return parsed;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { posix, join } from 'node:path';
|
|
7
7
|
import { getNextNumber, buildNumberedFilename } from './incrementing-number.js';
|
|
8
8
|
import { getSessionId } from '../services/session/session-manager.js';
|
|
9
|
+
import { findProjectRoot } from '../services/config/config-safety.js';
|
|
9
10
|
const CHANGE_ID_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
10
11
|
function normalizeForwardSlashes(input) {
|
|
11
12
|
return input.replace(/\\/g, '/');
|
|
@@ -75,10 +76,11 @@ export function isUnsafeArtifactPath(path) {
|
|
|
75
76
|
*/
|
|
76
77
|
export function buildArtifactRelativePath(changeId, ...segments) {
|
|
77
78
|
validateChangeIdOrThrow(changeId);
|
|
78
|
-
const
|
|
79
|
+
const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
|
|
80
|
+
const sessionId = getSessionId(projectRoot);
|
|
79
81
|
if (sessionId && segments.length > 0 && segments[0]) {
|
|
80
82
|
const role = normalizeForwardSlashes(segments[0]);
|
|
81
|
-
const dirPath = join(
|
|
83
|
+
const dirPath = join(projectRoot, '.peaks', sessionId, role);
|
|
82
84
|
if (isUnsafeArtifactPath(role) || isUnsafeArtifactPath(sessionId)) {
|
|
83
85
|
throw new ChangeIdValidationError(changeId);
|
|
84
86
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "1.0.
|
|
1
|
+
export declare const CLI_VERSION = "1.0.27";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.0.
|
|
1
|
+
export const CLI_VERSION = "1.0.27";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.27",
|
|
4
4
|
"description": "Peaks CLI and short skill family for Claude Code automation.",
|
|
5
5
|
"author": "SquabbyZ",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"dev:watch": "node ./scripts/watch.mjs",
|
|
36
36
|
"test": "vitest run",
|
|
37
37
|
"test:coverage": "vitest run --coverage",
|
|
38
|
-
"pretest:coverage": "
|
|
38
|
+
"pretest:coverage": "node ./scripts/pretest-coverage.mjs",
|
|
39
39
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
40
40
|
},
|
|
41
41
|
"engines": {
|
|
@@ -576,6 +576,14 @@ After `peaks-rd` finishes any implementation, repair, or code-output slice, Peak
|
|
|
576
576
|
|
|
577
577
|
Solo is itself a skill running in the current session. To "invoke peaks-rd" or "peaks-qa", Solo MUST use the `Skill` tool with the role's name (e.g. `Skill(skill="peaks-rd")` or `Skill(skill="peaks-qa")`), passing the `<request-id>` and `<session-id>` as arguments so the role reads the same artifacts Solo wrote. Do NOT re-implement the role's logic inline in Solo. Do NOT use the `Agent` tool with a sub-agent — role skills are skills, not agents. After the role skill returns, Solo reads the artifacts the role wrote (via the request artifact path or `peaks request show <rid> --role <role>`) to decide the next step.
|
|
578
578
|
|
|
579
|
+
**Presence restoration after role skill returns (MANDATORY):** Role skills (peaks-rd, peaks-qa, peaks-ui) call `peaks skill presence:set <role>` internally, which overwrites `.peaks/.active-skill.json`. After EVERY role skill returns — whether success, repair-needed, or failure — Solo MUST immediately restore the orchestrator presence by re-running the same presence command from Step 2:
|
|
580
|
+
|
|
581
|
+
```bash
|
|
582
|
+
peaks skill presence:set peaks-solo --mode <mode> --gate <current-gate>
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
This keeps the CLAUDE.md status header accurate (`Peaks-Cli Skill: peaks-solo`) instead of showing a stale role name. Use the current mode and gate values; the gate may have advanced since startup. Skipping this step causes the header to display the last role skill name permanently.
|
|
586
|
+
|
|
579
587
|
**Full-auto auto-proceed rule**: In the `full-auto` profile, when RD transitions to `qa-handoff`, Solo immediately invokes `peaks-qa` via the Skill tool with the same `<request-id>`. Do not pause, do not ask the user, do not summarize RD results as if they were final. The only valid reason to skip QA is when `--type` is `docs` or `chore` (no acceptance surface).
|
|
580
588
|
|
|
581
589
|
A QA report with any failing, blocked, missing, or unverified acceptance item is not a pass.
|
|
@@ -596,12 +604,16 @@ When `peaks-qa` returns `verdict=return-to-rd`, Solo does NOT manually rewrite R
|
|
|
596
604
|
4. peaks-rd fixes the reported issues only (red-line scope: do not modify unrelated surfaces), regenerates code-review and security-review evidence if changes touched reviewed surfaces, then transitions `rd → implemented → qa-handoff` again.
|
|
597
605
|
5. Solo invokes `peaks-qa` again with the same `<request-id>` (the same Skill call as before). QA re-runs gates against the new diff.
|
|
598
606
|
6. Repeat steps 1-5 until QA returns `verdict=pass`, or the cap below fires.
|
|
607
|
+
**After each repair iteration** (after peaks-rd and peaks-qa both return), Solo MUST restore presence:
|
|
608
|
+
```bash
|
|
609
|
+
peaks skill presence:set peaks-solo --mode <mode> --gate repair-cycle-<N>
|
|
610
|
+
```
|
|
599
611
|
|
|
600
612
|
**Repair cycle cap**: After 3 repair cycles without a passing QA verdict, emit a blocked TXT handoff regardless of remaining issues. Do not loop indefinitely. If a specific issue cannot be resolved within 3 cycles, mark it as a known blocker in the TXT handoff and proceed to the SC phase.
|
|
601
613
|
|
|
602
614
|
In full-auto mode, treat the RD↔QA repair loop as a built-in controller objective: loop through RD→QA until all acceptance items pass (max 3 cycles). Do not exit the loop on a non-passing QA verdict unless the TXT handoff marks the workflow as blocked.
|
|
603
615
|
|
|
604
|
-
##
|
|
616
|
+
## Default runbook
|
|
605
617
|
|
|
606
618
|
> **Maintenance**: The numbered workflow list above (steps 0-11) is the canonical phase sequence. This runbook is the executable CLI transcription. When updating this skill, keep both in lockstep — a change to one must be reflected in the other.
|
|
607
619
|
|
|
@@ -775,7 +787,7 @@ Do NOT call `peaks skill presence:clear` at workflow end. The presence file and
|
|
|
775
787
|
|
|
776
788
|
**Codegraph**: Optional project-analysis before RD handoff. Use `peaks codegraph affected --project <path> <changed-files...> --json` for regression-surface hints. Output as untrusted supporting evidence only; never commit `.codegraph/` artifacts.
|
|
777
789
|
|
|
778
|
-
##
|
|
790
|
+
## Codegraph orchestration context
|
|
779
791
|
|
|
780
792
|
Solo treats `peaks codegraph affected --project <path> <changed-files...> --json` as an optional project-analysis enhancement that informs the role handoff between PRD, RD, and QA. The output is untrusted supporting evidence — Solo must not treat codegraph output as approval for scope, design, or QA verdict.
|
|
781
793
|
|