peaks-cli 1.0.24 → 1.0.26
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/dist/src/cli/commands/core-artifact-commands.js +43 -1
- package/dist/src/services/session/index.d.ts +1 -1
- package/dist/src/services/session/index.js +1 -1
- package/dist/src/services/session/session-manager.d.ts +29 -0
- package/dist/src/services/session/session-manager.js +90 -2
- package/dist/src/services/skills/skill-presence-service.d.ts +1 -0
- package/dist/src/services/skills/skill-presence-service.js +31 -0
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/skills/peaks-solo/SKILL.md +24 -2
|
@@ -8,6 +8,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
10
|
import { setSkillPresence, clearSkillPresence, getSkillPresence, isSkillPresenceMode, touchSkillHeartbeat } from '../../services/skills/skill-presence-service.js';
|
|
11
|
+
import { ensureSession, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas } from '../../services/session/session-manager.js';
|
|
11
12
|
import { fail, ok } from '../../shared/result.js';
|
|
12
13
|
import { addJsonOption, failUnsupportedNonDryRun, getErrorMessage, isArtifactProvider, isArtifactSetupStep, printResult } from '../cli-helpers.js';
|
|
13
14
|
export function registerCoreAndArtifactCommands(program, io) {
|
|
@@ -71,13 +72,21 @@ export function registerCoreAndArtifactCommands(program, io) {
|
|
|
71
72
|
.command('presence:set <name>')
|
|
72
73
|
.description('Set the currently active Peaks skill for session-wide visibility')
|
|
73
74
|
.option('--mode <mode>', 'execution mode')
|
|
74
|
-
.option('--gate <gate>', 'current gate')).action((name, options) => {
|
|
75
|
+
.option('--gate <gate>', 'current gate')).action(async (name, options) => {
|
|
75
76
|
if (options.mode !== undefined && !isSkillPresenceMode(options.mode)) {
|
|
76
77
|
printResult(io, fail('skill.presence:set', 'INVALID_MODE', `Invalid mode: ${options.mode} (expected one of: full-auto, assisted, swarm, strict)`, { name, mode: options.mode }, ['Use a valid mode: full-auto, assisted, swarm, or strict']), options.json);
|
|
77
78
|
process.exitCode = 1;
|
|
78
79
|
return;
|
|
79
80
|
}
|
|
80
81
|
const presence = setSkillPresence(name, options.mode, options.gate);
|
|
82
|
+
// Also update session metadata so session dirs self-document
|
|
83
|
+
const projectRoot = process.cwd();
|
|
84
|
+
const sessionId = await ensureSession(projectRoot);
|
|
85
|
+
setSessionMeta(projectRoot, sessionId, {
|
|
86
|
+
skill: name,
|
|
87
|
+
...(options.mode ? { mode: options.mode } : {}),
|
|
88
|
+
...(options.gate ? { gate: options.gate } : {})
|
|
89
|
+
});
|
|
81
90
|
printResult(io, ok('skill.presence:set', { active: true, ...presence }), options.json);
|
|
82
91
|
});
|
|
83
92
|
addJsonOption(skill
|
|
@@ -116,6 +125,39 @@ export function registerCoreAndArtifactCommands(program, io) {
|
|
|
116
125
|
lastHeartbeat: updated.lastHeartbeat
|
|
117
126
|
}), options.json);
|
|
118
127
|
});
|
|
128
|
+
const session = program.command('session').description('Manage Peaks session directories');
|
|
129
|
+
addJsonOption(session
|
|
130
|
+
.command('list')
|
|
131
|
+
.description('List all session directories with titles and metadata')).action((options) => {
|
|
132
|
+
const projectRoot = process.cwd();
|
|
133
|
+
const metas = listSessionMetas(projectRoot);
|
|
134
|
+
printResult(io, ok('session.list', { sessions: metas, total: metas.length }), options.json);
|
|
135
|
+
});
|
|
136
|
+
addJsonOption(session
|
|
137
|
+
.command('info <sessionId>')
|
|
138
|
+
.description('Show full metadata for a session directory')).action((sessionId, options) => {
|
|
139
|
+
const projectRoot = process.cwd();
|
|
140
|
+
const meta = getSessionMeta(projectRoot, sessionId);
|
|
141
|
+
if (meta === null) {
|
|
142
|
+
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);
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
printResult(io, ok('session.info', meta), options.json);
|
|
147
|
+
});
|
|
148
|
+
addJsonOption(session
|
|
149
|
+
.command('title <sessionId> <title>')
|
|
150
|
+
.description('Set a human-readable title for a session directory')).action((sessionId, title, options) => {
|
|
151
|
+
const projectRoot = process.cwd();
|
|
152
|
+
try {
|
|
153
|
+
const meta = setSessionTitle(projectRoot, sessionId, title);
|
|
154
|
+
printResult(io, ok('session.title', meta), options.json);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
printResult(io, fail('session.title', 'SESSION_TITLE_FAILED', getErrorMessage(error), { sessionId }, ['Verify the sessionId exists under .peaks/']), options.json);
|
|
158
|
+
process.exitCode = 1;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
119
161
|
const profile = program.command('profile').description('Manage runtime profiles');
|
|
120
162
|
addJsonOption(profile.command('list').description('List available profiles')).action((options) => {
|
|
121
163
|
printResult(io, ok('profile.list', { profiles: listProfiles() }), options.json);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getProjectScanPath, hasProjectScan, type SessionInfo } from './session-manager.js';
|
|
1
|
+
export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, type SessionInfo, type SessionMeta } from './session-manager.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getProjectScanPath, hasProjectScan } from './session-manager.js';
|
|
1
|
+
export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan } from './session-manager.js';
|
|
@@ -10,6 +10,35 @@ export type SessionInfo = {
|
|
|
10
10
|
createdAt: string;
|
|
11
11
|
projectRoot: string;
|
|
12
12
|
};
|
|
13
|
+
export type SessionMeta = {
|
|
14
|
+
sessionId: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
skill?: string;
|
|
17
|
+
mode?: string;
|
|
18
|
+
gate?: string;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
lastActivity?: string;
|
|
21
|
+
projectRoot: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Read metadata for a specific session directory.
|
|
25
|
+
* Returns null if the session directory or its session.json does not exist.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getSessionMeta(projectRoot: string, sessionId: string): SessionMeta | null;
|
|
28
|
+
/**
|
|
29
|
+
* Write or update metadata for a session. Fields besides sessionId and createdAt
|
|
30
|
+
* are merged on top of the current meta (partial update).
|
|
31
|
+
*/
|
|
32
|
+
export declare function setSessionMeta(projectRoot: string, sessionId: string, partial: Partial<Omit<SessionMeta, 'sessionId' | 'createdAt' | 'projectRoot'>>): SessionMeta;
|
|
33
|
+
/**
|
|
34
|
+
* Set the display title for a session directory.
|
|
35
|
+
*/
|
|
36
|
+
export declare function setSessionTitle(projectRoot: string, sessionId: string, title: string): SessionMeta;
|
|
37
|
+
/**
|
|
38
|
+
* List all session directories under .peaks with their metadata.
|
|
39
|
+
* Returns sessions sorted by sessionId descending (most recent first).
|
|
40
|
+
*/
|
|
41
|
+
export declare function listSessionMetas(projectRoot: string): SessionMeta[];
|
|
13
42
|
/**
|
|
14
43
|
* Get or create the current session for a project.
|
|
15
44
|
* If a valid session already exists, returns it.
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
* Sessions are automatically created when any skill is invoked.
|
|
6
6
|
* Each session gets a unique directory under .peaks/ with incrementing numbered files.
|
|
7
7
|
*/
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { randomBytes } from 'node:crypto';
|
|
11
11
|
import { initWorkspace } from '../workspace/workspace-service.js';
|
|
12
12
|
const SESSION_FILE = '.session.json';
|
|
13
|
+
const META_FILE = 'session.json';
|
|
13
14
|
/**
|
|
14
15
|
* Generate a new session ID.
|
|
15
16
|
* Format: YYYY-MM-DD-session-<6位hex>
|
|
@@ -60,6 +61,86 @@ function writeSessionFile(projectRoot, info) {
|
|
|
60
61
|
}
|
|
61
62
|
writeFileSync(sessionFile, JSON.stringify(info, null, 2), 'utf8');
|
|
62
63
|
}
|
|
64
|
+
function getMetaFilePath(projectRoot, sessionId) {
|
|
65
|
+
return join(projectRoot, '.peaks', sessionId, META_FILE);
|
|
66
|
+
}
|
|
67
|
+
function readSessionMeta(projectRoot, sessionId) {
|
|
68
|
+
const metaPath = getMetaFilePath(projectRoot, sessionId);
|
|
69
|
+
if (!existsSync(metaPath))
|
|
70
|
+
return null;
|
|
71
|
+
try {
|
|
72
|
+
const raw = readFileSync(metaPath, 'utf8');
|
|
73
|
+
const parsed = JSON.parse(raw);
|
|
74
|
+
if (typeof parsed?.sessionId !== 'string' || parsed.sessionId.length === 0) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return parsed;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function writeSessionMeta(projectRoot, sessionId, meta) {
|
|
84
|
+
const metaPath = getMetaFilePath(projectRoot, sessionId);
|
|
85
|
+
const metaDir = join(projectRoot, '.peaks', sessionId);
|
|
86
|
+
if (!existsSync(metaDir)) {
|
|
87
|
+
mkdirSync(metaDir, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf8');
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Read metadata for a specific session directory.
|
|
93
|
+
* Returns null if the session directory or its session.json does not exist.
|
|
94
|
+
*/
|
|
95
|
+
export function getSessionMeta(projectRoot, sessionId) {
|
|
96
|
+
return readSessionMeta(projectRoot, sessionId);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Write or update metadata for a session. Fields besides sessionId and createdAt
|
|
100
|
+
* are merged on top of the current meta (partial update).
|
|
101
|
+
*/
|
|
102
|
+
export function setSessionMeta(projectRoot, sessionId, partial) {
|
|
103
|
+
const existing = readSessionMeta(projectRoot, sessionId);
|
|
104
|
+
const now = new Date().toISOString();
|
|
105
|
+
const meta = existing
|
|
106
|
+
? { ...existing, ...partial, lastActivity: now }
|
|
107
|
+
: {
|
|
108
|
+
sessionId,
|
|
109
|
+
projectRoot,
|
|
110
|
+
createdAt: now,
|
|
111
|
+
...partial,
|
|
112
|
+
lastActivity: now
|
|
113
|
+
};
|
|
114
|
+
writeSessionMeta(projectRoot, sessionId, meta);
|
|
115
|
+
return meta;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Set the display title for a session directory.
|
|
119
|
+
*/
|
|
120
|
+
export function setSessionTitle(projectRoot, sessionId, title) {
|
|
121
|
+
return setSessionMeta(projectRoot, sessionId, { title });
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* List all session directories under .peaks with their metadata.
|
|
125
|
+
* Returns sessions sorted by sessionId descending (most recent first).
|
|
126
|
+
*/
|
|
127
|
+
export function listSessionMetas(projectRoot) {
|
|
128
|
+
const peaksRoot = join(projectRoot, '.peaks');
|
|
129
|
+
if (!existsSync(peaksRoot))
|
|
130
|
+
return [];
|
|
131
|
+
const entries = readdirSync(peaksRoot, { withFileTypes: true });
|
|
132
|
+
return entries
|
|
133
|
+
.filter((entry) => entry.isDirectory() && /^\d{4}-\d{2}-\d{2}-session-[a-f0-9]+$/.test(entry.name))
|
|
134
|
+
.map((entry) => {
|
|
135
|
+
const meta = readSessionMeta(projectRoot, entry.name);
|
|
136
|
+
return meta ?? {
|
|
137
|
+
sessionId: entry.name,
|
|
138
|
+
projectRoot,
|
|
139
|
+
createdAt: ''
|
|
140
|
+
};
|
|
141
|
+
})
|
|
142
|
+
.sort((a, b) => b.sessionId.localeCompare(a.sessionId));
|
|
143
|
+
}
|
|
63
144
|
/**
|
|
64
145
|
* Get or create the current session for a project.
|
|
65
146
|
* If a valid session already exists, returns it.
|
|
@@ -74,13 +155,20 @@ export async function ensureSession(projectRoot) {
|
|
|
74
155
|
return existing.sessionId;
|
|
75
156
|
}
|
|
76
157
|
const sessionId = generateSessionId();
|
|
158
|
+
const now = new Date().toISOString();
|
|
77
159
|
const info = {
|
|
78
160
|
sessionId,
|
|
79
|
-
createdAt:
|
|
161
|
+
createdAt: now,
|
|
80
162
|
projectRoot
|
|
81
163
|
};
|
|
82
164
|
writeSessionFile(projectRoot, info);
|
|
83
165
|
await initWorkspace({ projectRoot, sessionId });
|
|
166
|
+
// Initialize session metadata inside the session directory
|
|
167
|
+
writeSessionMeta(projectRoot, sessionId, {
|
|
168
|
+
sessionId,
|
|
169
|
+
projectRoot,
|
|
170
|
+
createdAt: now
|
|
171
|
+
});
|
|
84
172
|
return sessionId;
|
|
85
173
|
}
|
|
86
174
|
/**
|
|
@@ -10,19 +10,36 @@ export function isSkillPresenceMode(value) {
|
|
|
10
10
|
return VALID_SKILL_PRESENCE_MODES.includes(value);
|
|
11
11
|
}
|
|
12
12
|
const PRESENCE_FILE = '.peaks/.active-skill.json';
|
|
13
|
+
const SESSION_FILE = '.peaks/.session.json';
|
|
13
14
|
function resolvePresencePath() {
|
|
14
15
|
return resolve(process.cwd(), PRESENCE_FILE);
|
|
15
16
|
}
|
|
17
|
+
function getCurrentSessionId() {
|
|
18
|
+
const sessionPath = resolve(process.cwd(), SESSION_FILE);
|
|
19
|
+
if (!existsSync(sessionPath))
|
|
20
|
+
return null;
|
|
21
|
+
try {
|
|
22
|
+
const data = JSON.parse(readFileSync(sessionPath, 'utf8'));
|
|
23
|
+
return typeof data.sessionId === 'string' && data.sessionId.length > 0
|
|
24
|
+
? data.sessionId
|
|
25
|
+
: null;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
16
31
|
export function exportSkillPresence() {
|
|
17
32
|
return resolvePresencePath();
|
|
18
33
|
}
|
|
19
34
|
export function setSkillPresence(skill, mode, gate) {
|
|
20
35
|
const validatedMode = mode && isSkillPresenceMode(mode) ? mode : undefined;
|
|
36
|
+
const sessionId = getCurrentSessionId();
|
|
21
37
|
const now = new Date().toISOString();
|
|
22
38
|
const presence = {
|
|
23
39
|
skill,
|
|
24
40
|
...(validatedMode ? { mode: validatedMode } : {}),
|
|
25
41
|
...(gate ? { gate } : {}),
|
|
42
|
+
...(sessionId ? { sessionId } : {}),
|
|
26
43
|
setAt: now,
|
|
27
44
|
lastHeartbeat: now
|
|
28
45
|
};
|
|
@@ -45,6 +62,13 @@ export function getSkillPresence() {
|
|
|
45
62
|
if (typeof parsed?.skill !== 'string' || parsed.skill.length === 0) {
|
|
46
63
|
return null;
|
|
47
64
|
}
|
|
65
|
+
if (typeof parsed.sessionId === 'string' && parsed.sessionId.length > 0) {
|
|
66
|
+
const currentSessionId = getCurrentSessionId();
|
|
67
|
+
if (currentSessionId && parsed.sessionId !== currentSessionId) {
|
|
68
|
+
unlinkSync(presencePath);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
48
72
|
return parsed;
|
|
49
73
|
}
|
|
50
74
|
catch {
|
|
@@ -62,6 +86,13 @@ export function touchSkillHeartbeat() {
|
|
|
62
86
|
if (typeof parsed?.skill !== 'string' || parsed.skill.length === 0) {
|
|
63
87
|
return null;
|
|
64
88
|
}
|
|
89
|
+
if (typeof parsed.sessionId === 'string' && parsed.sessionId.length > 0) {
|
|
90
|
+
const currentSessionId = getCurrentSessionId();
|
|
91
|
+
if (currentSessionId && parsed.sessionId !== currentSessionId) {
|
|
92
|
+
unlinkSync(presencePath);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
65
96
|
parsed.lastHeartbeat = new Date().toISOString();
|
|
66
97
|
writeFileSync(presencePath, JSON.stringify(parsed, null, 2), 'utf8');
|
|
67
98
|
return parsed;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "1.0.
|
|
1
|
+
export declare const CLI_VERSION = "1.0.26";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.0.
|
|
1
|
+
export const CLI_VERSION = "1.0.26";
|
package/package.json
CHANGED
|
@@ -78,6 +78,16 @@ Then display the compact status header: `Peaks-Cli Skill: peaks-solo | Peaks-Cli
|
|
|
78
78
|
|
|
79
79
|
Update with `peaks skill presence:set peaks-solo --mode <mode> --gate <gate>` when gates change. The presence file persists across the full workflow lifecycle — do NOT clear it at workflow end.
|
|
80
80
|
|
|
81
|
+
### Peaks-Cli Step 2.5: Set session title
|
|
82
|
+
|
|
83
|
+
Extract a short (8-20 Chinese characters, or 4-10 English words) descriptive title from the user's first request. The title should capture the core task — e.g. "修复登录页OAuth回调异常", "添加暗色模式开关", "搭建项目基础架构". Then run:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
peaks session title $(cat .peaks/.session.json | python3 -c "import sys,json; print(json.load(sys.stdin)['sessionId'])") "<title>"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
If the session directory already has a title (check via `peaks session list --json`), skip this step — the title is already set.
|
|
90
|
+
|
|
81
91
|
## Boundaries
|
|
82
92
|
|
|
83
93
|
Peaks-Cli Solo may:
|
|
@@ -566,6 +576,14 @@ After `peaks-rd` finishes any implementation, repair, or code-output slice, Peak
|
|
|
566
576
|
|
|
567
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.
|
|
568
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
|
+
|
|
569
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).
|
|
570
588
|
|
|
571
589
|
A QA report with any failing, blocked, missing, or unverified acceptance item is not a pass.
|
|
@@ -586,12 +604,16 @@ When `peaks-qa` returns `verdict=return-to-rd`, Solo does NOT manually rewrite R
|
|
|
586
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.
|
|
587
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.
|
|
588
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
|
+
```
|
|
589
611
|
|
|
590
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.
|
|
591
613
|
|
|
592
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.
|
|
593
615
|
|
|
594
|
-
##
|
|
616
|
+
## Default runbook
|
|
595
617
|
|
|
596
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.
|
|
597
619
|
|
|
@@ -765,7 +787,7 @@ Do NOT call `peaks skill presence:clear` at workflow end. The presence file and
|
|
|
765
787
|
|
|
766
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.
|
|
767
789
|
|
|
768
|
-
##
|
|
790
|
+
## Codegraph orchestration context
|
|
769
791
|
|
|
770
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.
|
|
771
793
|
|