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.
@@ -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
- if (projectRoot === null)
30
- return false;
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
- return planStatusLineInstall('project', projectRoot).alreadyInstalled;
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();
@@ -6,6 +6,7 @@ export type SkillPresence = {
6
6
  mode?: SkillPresenceMode;
7
7
  gate?: string;
8
8
  sessionId?: string;
9
+ claudeSessionId?: string;
9
10
  setAt: string;
10
11
  lastHeartbeat?: string;
11
12
  };
@@ -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.0";
1
+ export declare const CLI_VERSION = "1.2.1";
@@ -1 +1 @@
1
- export const CLI_VERSION = "1.2.0";
1
+ export const CLI_VERSION = "1.2.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-cli",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Peaks CLI and short skill family for Claude Code automation.",
5
5
  "author": "SquabbyZ",
6
6
  "license": "MIT",
@@ -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> (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)."
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 1: Mode selection (MUST run before presence:set)
47
+ ### Peaks-Cli Step 0: Anchor the workflow (MANDATORY FIRST ACTIONS — no bail-out)
48
48
 
49
- When the user invokes Peaks-Cli Solo without explicitly naming an execution profile, use `AskUserQuestion` BEFORE any other action. Present the recommended full-auto path as the first/default option with a practical description for each:
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: Set skill presence
95
+ ### Peaks-Cli Step 2: Re-set skill presence with the chosen mode
70
96
 
71
- Only after the mode is known (user selected or explicitly named), run:
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
- Before ANY role handoff or artifact write, Peaks-Cli Solo MUST create the workspace. 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`.
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;