peaks-cli 1.2.0 → 1.2.1
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/services/doctor/doctor-service.d.ts +4 -0
- package/dist/src/services/doctor/doctor-service.js +66 -3
- package/dist/src/services/skills/skill-presence-service.d.ts +1 -0
- package/dist/src/services/skills/skill-presence-service.js +12 -0
- package/dist/src/services/skills/skill-statusline-service.d.ts +2 -0
- package/dist/src/services/skills/skill-statusline-service.js +11 -1
- 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 +1 -1
- package/skills/peaks-solo/SKILL.md +32 -6
|
@@ -25,5 +25,9 @@ export type DoctorOptions = {
|
|
|
25
25
|
skillPresenceProbe?: () => SkillPresence | null;
|
|
26
26
|
skillPresenceFreshnessThresholdMs?: number;
|
|
27
27
|
statusLineInstalledProbe?: () => boolean;
|
|
28
|
+
/** Returns true when a Peaks workspace session (.peaks/.session.json) exists. */
|
|
29
|
+
workspaceInitializedProbe?: () => boolean;
|
|
30
|
+
/** Platform string (defaults to process.platform); injectable for tests. */
|
|
31
|
+
platform?: NodeJS.Platform;
|
|
28
32
|
};
|
|
29
33
|
export declare function runDoctor(options?: DoctorOptions): Promise<DoctorReport>;
|
|
@@ -10,6 +10,7 @@ import { loadSkillRegistry } from '../skills/skill-registry.js';
|
|
|
10
10
|
import { getSkillPresence } from '../skills/skill-presence-service.js';
|
|
11
11
|
import { planStatusLineInstall } from '../skills/statusline-settings-service.js';
|
|
12
12
|
import { findProjectRoot } from '../config/config-safety.js';
|
|
13
|
+
import { CLI_VERSION } from '../../shared/version.js';
|
|
13
14
|
const CODEGRAPH_EXPECTED_VERSION = '0.7.10';
|
|
14
15
|
const SKILL_PRESENCE_FRESHNESS_THRESHOLD_MS = 24 * 60 * 60 * 1000;
|
|
15
16
|
function defaultCodegraphProbe() {
|
|
@@ -26,15 +27,29 @@ function defaultCodegraphProbe() {
|
|
|
26
27
|
}
|
|
27
28
|
function defaultStatusLineInstalledProbe() {
|
|
28
29
|
const projectRoot = findProjectRoot(process.cwd());
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
// Check both scopes: a user may have installed the statusLine globally, which
|
|
31
|
+
// the project-only check would miss and falsely report as "not installed".
|
|
31
32
|
try {
|
|
32
|
-
|
|
33
|
+
if (projectRoot !== null && planStatusLineInstall('project', projectRoot).alreadyInstalled) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* fall through to global */
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
return planStatusLineInstall('global').alreadyInstalled;
|
|
33
42
|
}
|
|
34
43
|
catch {
|
|
35
44
|
return false;
|
|
36
45
|
}
|
|
37
46
|
}
|
|
47
|
+
function defaultWorkspaceInitializedProbe() {
|
|
48
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
49
|
+
if (projectRoot === null)
|
|
50
|
+
return false;
|
|
51
|
+
return existsSync(join(projectRoot, '.peaks', '.session.json'));
|
|
52
|
+
}
|
|
38
53
|
const DESTRUCTIVE_APPLY_PATTERNS = [
|
|
39
54
|
/peaks\s+memory\s+sync[^\n]*--apply/,
|
|
40
55
|
/peaks\s+memory\s+extract[^\n]*--apply/,
|
|
@@ -203,6 +218,34 @@ export async function runDoctor(options = {}) {
|
|
|
203
218
|
}
|
|
204
219
|
}
|
|
205
220
|
}
|
|
221
|
+
// Workspace guard: an active workflow presence (peaks-solo) with no workspace
|
|
222
|
+
// session means the skill was anchored but `peaks workspace init` never ran —
|
|
223
|
+
// the #1 reported failure where .peaks/ artifacts are never created. This
|
|
224
|
+
// turns the SKILL.md "MUST create the workspace" prose into an executable check.
|
|
225
|
+
const workspaceProbe = options.workspaceInitializedProbe ?? defaultWorkspaceInitializedProbe;
|
|
226
|
+
let workspaceInitialized = false;
|
|
227
|
+
try {
|
|
228
|
+
workspaceInitialized = workspaceProbe();
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
workspaceInitialized = false;
|
|
232
|
+
}
|
|
233
|
+
if (presence !== null && !workspaceInitialized) {
|
|
234
|
+
checks.push({
|
|
235
|
+
id: 'skill-presence:workspace',
|
|
236
|
+
ok: false,
|
|
237
|
+
message: `Skill ${presence.skill} is active but no workspace session exists (.peaks/.session.json missing); run \`peaks workspace init --project <repo>\` — peaks-solo Step 0 must anchor the workspace before any work`
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
checks.push({
|
|
242
|
+
id: 'skill-presence:workspace',
|
|
243
|
+
ok: true,
|
|
244
|
+
message: presence === null
|
|
245
|
+
? 'No active skill presence; workspace guard not applicable'
|
|
246
|
+
: `Workspace session present for active skill ${presence.skill}`
|
|
247
|
+
});
|
|
248
|
+
}
|
|
206
249
|
// Discoverability nudge: when a skill is actively orchestrating but the
|
|
207
250
|
// out-of-band statusLine isn't installed, the user has no terminal-level
|
|
208
251
|
// signal that Peaks is in control. Suggest installing it (non-failing).
|
|
@@ -230,6 +273,26 @@ export async function runDoctor(options = {}) {
|
|
|
230
273
|
: 'Peaks statusLine not installed (no active skill; install optional)'
|
|
231
274
|
});
|
|
232
275
|
}
|
|
276
|
+
// Runtime/platform diagnostic for the "statusLine shows nothing" reports.
|
|
277
|
+
// Surfaces (a) the running peaks version — a stale global install predating
|
|
278
|
+
// the statusLine feature is a common cause — and (b) on Windows, the fact that
|
|
279
|
+
// the bare `peaks statusline` command must resolve in the shell Claude Code
|
|
280
|
+
// spawns, which fails when the npm global bin dir is not on that shell's PATH.
|
|
281
|
+
const platform = options.platform ?? process.platform;
|
|
282
|
+
if (platform === 'win32') {
|
|
283
|
+
checks.push({
|
|
284
|
+
id: 'statusline:runtime',
|
|
285
|
+
ok: true,
|
|
286
|
+
message: `peaks ${CLI_VERSION} (win32): if the statusLine shows nothing in git bash, verify \`peaks\` resolves on PATH in the shell Claude Code uses (run \`peaks -v\` there), reinstall globally with \`npm i -g peaks-cli@latest\` if the version is older than ${CLI_VERSION}, then re-run \`peaks statusline install\` and reload Claude Code`
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
checks.push({
|
|
291
|
+
id: 'statusline:runtime',
|
|
292
|
+
ok: true,
|
|
293
|
+
message: `peaks ${CLI_VERSION} (${platform}): statusLine command is \`peaks statusline\``
|
|
294
|
+
});
|
|
295
|
+
}
|
|
233
296
|
const probe = options.codegraphProbe ?? defaultCodegraphProbe;
|
|
234
297
|
try {
|
|
235
298
|
const result = probe();
|
|
@@ -10,6 +10,16 @@ export const VALID_SKILL_PRESENCE_MODES = [
|
|
|
10
10
|
export function isSkillPresenceMode(value) {
|
|
11
11
|
return VALID_SKILL_PRESENCE_MODES.includes(value);
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* The current Claude Code session id, exposed to Bash tool calls via the
|
|
15
|
+
* CLAUDE_CODE_SESSION_ID environment variable. Stamping it onto the presence
|
|
16
|
+
* file lets the read-only status line tell whether the recorded skill belongs
|
|
17
|
+
* to the live session (show it) or a previous one (render idle).
|
|
18
|
+
*/
|
|
19
|
+
function getCurrentClaudeSessionId() {
|
|
20
|
+
const value = process.env.CLAUDE_CODE_SESSION_ID;
|
|
21
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
22
|
+
}
|
|
13
23
|
const PRESENCE_FILE = '.peaks/.active-skill.json';
|
|
14
24
|
const SESSION_FILE = '.peaks/.session.json';
|
|
15
25
|
function resolveProjectRoot(override) {
|
|
@@ -40,12 +50,14 @@ export function exportSkillPresence(projectRootOverride) {
|
|
|
40
50
|
export function setSkillPresence(skill, mode, gate, projectRootOverride) {
|
|
41
51
|
const validatedMode = mode && isSkillPresenceMode(mode) ? mode : undefined;
|
|
42
52
|
const sessionId = getCurrentSessionId(projectRootOverride);
|
|
53
|
+
const claudeSessionId = getCurrentClaudeSessionId();
|
|
43
54
|
const now = new Date().toISOString();
|
|
44
55
|
const presence = {
|
|
45
56
|
skill,
|
|
46
57
|
...(validatedMode ? { mode: validatedMode } : {}),
|
|
47
58
|
...(gate ? { gate } : {}),
|
|
48
59
|
...(sessionId ? { sessionId } : {}),
|
|
60
|
+
...(claudeSessionId ? { claudeSessionId } : {}),
|
|
49
61
|
setAt: now,
|
|
50
62
|
lastHeartbeat: now
|
|
51
63
|
};
|
|
@@ -4,6 +4,7 @@ export type StatusLineStdin = {
|
|
|
4
4
|
project_dir?: string;
|
|
5
5
|
};
|
|
6
6
|
cwd?: string;
|
|
7
|
+
session_id?: string;
|
|
7
8
|
};
|
|
8
9
|
export type StatusLineState = 'active' | 'idle' | 'stale' | 'invalid-presence';
|
|
9
10
|
export type StatusLinePresence = {
|
|
@@ -11,6 +12,7 @@ export type StatusLinePresence = {
|
|
|
11
12
|
mode?: string;
|
|
12
13
|
gate?: string;
|
|
13
14
|
setAt?: string;
|
|
15
|
+
claudeSessionId?: string;
|
|
14
16
|
};
|
|
15
17
|
export type StatusLineModel = {
|
|
16
18
|
state: StatusLineState;
|
|
@@ -65,7 +65,8 @@ function readPresenceReadOnly(projectRoot) {
|
|
|
65
65
|
skill: candidate.skill,
|
|
66
66
|
...(typeof candidate.mode === 'string' ? { mode: candidate.mode } : {}),
|
|
67
67
|
...(typeof candidate.gate === 'string' ? { gate: candidate.gate } : {}),
|
|
68
|
-
...(typeof candidate.setAt === 'string' ? { setAt: candidate.setAt } : {})
|
|
68
|
+
...(typeof candidate.setAt === 'string' ? { setAt: candidate.setAt } : {}),
|
|
69
|
+
...(typeof candidate.claudeSessionId === 'string' ? { claudeSessionId: candidate.claudeSessionId } : {})
|
|
69
70
|
},
|
|
70
71
|
invalid: false
|
|
71
72
|
};
|
|
@@ -87,6 +88,15 @@ export function buildStatusLineModel(stdin, nowMs) {
|
|
|
87
88
|
if (presence === null) {
|
|
88
89
|
return { state: 'idle', projectRoot, presence: null, ageMs: null };
|
|
89
90
|
}
|
|
91
|
+
// Session binding: when the presence was stamped with a Claude session id and
|
|
92
|
+
// the live session (from stdin) is a different one, the recorded skill belongs
|
|
93
|
+
// to a previous session — render idle instead of a stale "active" skill. When
|
|
94
|
+
// either id is absent (legacy presence, or harness that omits session_id) we
|
|
95
|
+
// fall back to the time-based behavior below for backward compatibility.
|
|
96
|
+
const liveSessionId = typeof stdin?.session_id === 'string' && stdin.session_id.length > 0 ? stdin.session_id : null;
|
|
97
|
+
if (presence.claudeSessionId && liveSessionId && presence.claudeSessionId !== liveSessionId) {
|
|
98
|
+
return { state: 'idle', projectRoot, presence: null, ageMs: null };
|
|
99
|
+
}
|
|
90
100
|
const setAtMs = presence.setAt ? Date.parse(presence.setAt) : Number.NaN;
|
|
91
101
|
const ageMs = Number.isNaN(setAtMs) ? null : nowMs - setAtMs;
|
|
92
102
|
const state = ageMs !== null && ageMs > STALE_THRESHOLD_MS ? 'stale' : 'active';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "1.2.
|
|
1
|
+
export declare const CLI_VERSION = "1.2.1";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.2.
|
|
1
|
+
export const CLI_VERSION = "1.2.1";
|
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"id": {
|
|
15
15
|
"type": "string",
|
|
16
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> (
|
|
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/workspace), statusline:<topic> (out-of-band Claude Code statusLine — install/runtime), 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 }
|
|
@@ -44,9 +44,35 @@ peaks-solo (orchestrate only)
|
|
|
44
44
|
|
|
45
45
|
## Peaks-Cli Startup sequence (MANDATORY — execute in order)
|
|
46
46
|
|
|
47
|
-
### Peaks-Cli Step
|
|
47
|
+
### Peaks-Cli Step 0: Anchor the workflow (MANDATORY FIRST ACTIONS — no bail-out)
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
The instant Peaks-Cli Solo is invoked, **before** the mode-selection question, before any analysis, and before you decide whether the request "needs" the full pipeline, you MUST run these two commands and see their output:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Session ID is auto-generated when omitted; the command returns it in the JSON output
|
|
53
|
+
peaks workspace init --project <repo> --json
|
|
54
|
+
peaks skill presence:set peaks-solo --project <repo> --gate startup
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If `workspace init` fails with "required option '--session-id' not specified", the CLI version predates auto-generation. Generate a session ID manually and pass it:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
SESSION_ID="$(date +%Y-%m-%d)-session-$(openssl rand -hex 3)"
|
|
61
|
+
peaks workspace init --project <repo> --session-id "$SESSION_ID" --json
|
|
62
|
+
peaks skill presence:set peaks-solo --project <repo> --gate startup
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
> `<repo>` is the **git project root** (the directory containing `.git`). In a monorepo / single-repo-multi-package layout, this is the repo root, NOT a sub-package — `.peaks/` lives at the repo root so every package shares one workspace. If unsure, run `git rev-parse --show-toplevel` and use that path. Never let `.peaks/` land inside a sub-package directory.
|
|
66
|
+
|
|
67
|
+
**There is no request too lightweight to skip this.** "分析下这个项目", "看一下代码", "分析项目", "解释一下架构", a one-line question — all of them still create the workspace and set presence first. The workspace is cheap; a missing `.peaks/` is the #1 reported failure.
|
|
68
|
+
|
|
69
|
+
**Anti-bail-out rule (BLOCKING):** You MUST NOT exit the peaks-solo workflow, hand control back, or produce a final answer before Step 0 has run. If you catch yourself thinking "this is just analysis, I don't need the workflow" — STOP. Run Step 0, set presence, then continue. A pure-analysis request runs the **lightweight analysis branch** (project scan + standards dry-run + handoff with a Standards-increment section), but it still anchors the workspace and keeps presence active. Declining to anchor is a workflow violation.
|
|
70
|
+
|
|
71
|
+
`presence:set` accepts no `--mode` here on purpose — mode is unknown until Step 1. It is re-run with the selected mode in Step 2. Setting presence early guarantees the status header/line shows `peaks-solo` from the very first turn even if the user never reaches mode selection.
|
|
72
|
+
|
|
73
|
+
### Peaks-Cli Step 1: Mode selection
|
|
74
|
+
|
|
75
|
+
After Step 0 has anchored the workspace and presence, when the user invokes Peaks-Cli Solo without explicitly naming an execution profile, use `AskUserQuestion` to pick the profile. Present the recommended full-auto path as the first/default option with a practical description for each:
|
|
50
76
|
|
|
51
77
|
1. **Full auto (Recommended)** — Peaks-Cli handles planning, role coordination, validation, and compact handoff end-to-end while preserving required confirmation gates for risky or shared-state actions.
|
|
52
78
|
2. **Assisted** — Peaks-Cli proposes plans, artifacts, and checks, then pauses for user decisions at major workflow boundaries.
|
|
@@ -66,9 +92,9 @@ Map the user's selection to the `--mode` flag value (used by `peaks skill presen
|
|
|
66
92
|
|
|
67
93
|
If the user already names a profile in their invocation (e.g. `/peaks-solo --full-auto`, "用全自动模式"), skip this question and use the named profile directly.
|
|
68
94
|
|
|
69
|
-
### Peaks-Cli Step 2:
|
|
95
|
+
### Peaks-Cli Step 2: Re-set skill presence with the chosen mode
|
|
70
96
|
|
|
71
|
-
|
|
97
|
+
Step 0 already set presence with no mode. Now that the mode is known (user selected or explicitly named), re-run presence:set so the header/status line shows the profile:
|
|
72
98
|
|
|
73
99
|
```bash
|
|
74
100
|
peaks skill presence:set peaks-solo --project <repo> --mode <mode-value> --gate startup
|
|
@@ -144,7 +170,7 @@ For frontend workflows, RD and QA must use Playwright MCP (`mcp__playwright__` t
|
|
|
144
170
|
|
|
145
171
|
### Workspace initialization gate
|
|
146
172
|
|
|
147
|
-
|
|
173
|
+
The workspace is created in Step 0 (Startup sequence) as a mandatory first action — before any analysis, role handoff, or artifact write, and regardless of how lightweight the request is. Session IDs are now **auto-generated** with the format `YYYY-MM-DD-session-<6位hex>` (e.g. `2026-05-26-session-a3f8b1`). The user does not provide a session ID — the system creates and persists it in `.peaks/.session.json`.
|
|
148
174
|
|
|
149
175
|
When `peaks workspace init` is run without `--session-id`, it automatically generates a new session ID using today's date and a random hex suffix. If `.peaks/.session.json` already exists with a valid session, the existing session is reused.
|
|
150
176
|
|
|
@@ -760,7 +786,7 @@ Use `standards init` for first-time creation and `standards update` for existing
|
|
|
760
786
|
|
|
761
787
|
Do not hand-write standards file mutations inside the skill.
|
|
762
788
|
|
|
763
|
-
For project-analysis requests such as "分析项目", the handoff must include an explicit **Standards increment** section. Report the current `CLAUDE.md` and `.claude/rules/**` status from the dry-run output as incremental deltas, not just a generic preflight note:
|
|
789
|
+
For project-analysis requests such as "分析项目" / "分析下这个项目", Step 0 still applies: the workspace is initialized and `peaks-solo` presence is set before any analysis output. These requests run the lightweight analysis branch (project scan + standards dry-run) rather than the full RD/QA pipeline, but they never skip workspace anchoring or exit the workflow. The handoff must include an explicit **Standards increment** section. Report the current `CLAUDE.md` and `.claude/rules/**` status from the dry-run output as incremental deltas, not just a generic preflight note:
|
|
764
790
|
|
|
765
791
|
- whether `CLAUDE.md` is missing, existing, planned, skipped, appended, or review-only;
|
|
766
792
|
- which `.claude/rules/**` files are planned, existing, skipped, appended, or review-only;
|