peaks-cli 1.0.22 → 1.0.24

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.
Files changed (33) hide show
  1. package/bin/peaks.js +0 -0
  2. package/dist/src/cli/commands/capability-commands.d.ts +11 -0
  3. package/dist/src/cli/commands/capability-commands.js +7 -6
  4. package/dist/src/cli/commands/core-artifact-commands.js +31 -1
  5. package/dist/src/cli/commands/workflow-commands.js +26 -0
  6. package/dist/src/services/artifacts/workspace-service.d.ts +1 -1
  7. package/dist/src/services/artifacts/workspace-service.js +2 -11
  8. package/dist/src/services/config/config-safety.d.ts +1 -1
  9. package/dist/src/services/config/config-safety.js +4 -6
  10. package/dist/src/services/config/config-service.d.ts +1 -1
  11. package/dist/src/services/config/config-service.js +17 -4
  12. package/dist/src/services/scan/acceptance-coverage-service.js +1 -4
  13. package/dist/src/services/scan/archetype-service.js +4 -15
  14. package/dist/src/services/scan/diff-scope-service.js +3 -3
  15. package/dist/src/services/scan/file-size-scan.js +2 -7
  16. package/dist/src/services/scan/type-sanity-service.js +1 -3
  17. package/dist/src/services/skills/skill-presence-service.d.ts +2 -0
  18. package/dist/src/services/skills/skill-presence-service.js +22 -1
  19. package/dist/src/services/standards/project-standards-service.js +21 -0
  20. package/dist/src/services/workflow/pipeline-verify-service.d.ts +31 -0
  21. package/dist/src/services/workflow/pipeline-verify-service.js +180 -0
  22. package/dist/src/shared/change-id.js +0 -3
  23. package/dist/src/shared/version.d.ts +1 -1
  24. package/dist/src/shared/version.js +1 -1
  25. package/output-styles/peaks-skill-swarm.md +34 -34
  26. package/package.json +2 -1
  27. package/skills/peaks-prd/SKILL.md +10 -10
  28. package/skills/peaks-qa/SKILL.md +48 -36
  29. package/skills/peaks-rd/SKILL.md +53 -53
  30. package/skills/peaks-sc/SKILL.md +9 -9
  31. package/skills/peaks-solo/SKILL.md +127 -91
  32. package/skills/peaks-txt/SKILL.md +17 -17
  33. package/skills/peaks-ui/SKILL.md +14 -14
package/bin/peaks.js CHANGED
File without changes
@@ -1,5 +1,16 @@
1
1
  import { Command } from 'commander';
2
2
  import type { PeaksConfig } from '../../services/config/config-types.js';
3
+ import type { CapabilityMapSourceFilter } from '../../services/recommendations/recommendation-types.js';
3
4
  import { type ProgramIO } from '../cli-helpers.js';
5
+ type CapabilityMapOptions = {
6
+ json?: boolean;
7
+ source: string;
8
+ };
4
9
  export declare function registerCapabilityCommands(program: Command, io: ProgramIO): void;
10
+ export declare function runCapabilityStatus(io: ProgramIO, options: {
11
+ json?: boolean;
12
+ }): void;
13
+ export declare function runCapabilityMap(io: ProgramIO, options: CapabilityMapOptions): void;
5
14
  export declare function getInstalledCapabilityIds(_config: PeaksConfig): string[];
15
+ export declare function parseCapabilityMapSource(source: string): CapabilityMapSourceFilter | null;
16
+ export {};
@@ -7,17 +7,18 @@ import { addJsonOption, printResult } from '../cli-helpers.js';
7
7
  const CAPABILITY_SOURCE_FILTERS = new Set(['all', 'access-repo', 'mcp-server']);
8
8
  export function registerCapabilityCommands(program, io) {
9
9
  const capability = program.command('capability').description('Inspect Peaks capability catalog and runtime availability');
10
- addJsonOption(capability.command('status').description('Show seed capability availability')).action((options) => {
11
- const availability = resolveCapabilityAvailability(seedCapabilityItems);
12
- printResult(io, ok('capability.status', { sources: seedCapabilitySources, items: seedCapabilityItems, availability }), options.json);
13
- });
10
+ addJsonOption(capability.command("status").description("Show seed capability availability")).action((options) => runCapabilityStatus(io, options));
14
11
  addCapabilityMapOptions(capability.command('map').description('Show dry-run external capability landing map')).action((options) => runCapabilityMap(io, options));
15
12
  addCapabilityMapOptions(program.command('capabilities').description('Show dry-run external capability landing map')).action((options) => runCapabilityMap(io, options));
16
13
  }
14
+ export function runCapabilityStatus(io, options) {
15
+ const availability = resolveCapabilityAvailability(seedCapabilityItems);
16
+ printResult(io, ok("capability.status", { sources: seedCapabilitySources, items: seedCapabilityItems, availability }), options.json);
17
+ }
17
18
  function addCapabilityMapOptions(command) {
18
19
  return addJsonOption(command.option('--source <source>', 'Filter source group: all, access-repo, or mcp-server', 'all'));
19
20
  }
20
- function runCapabilityMap(io, options) {
21
+ export function runCapabilityMap(io, options) {
21
22
  const source = parseCapabilityMapSource(options.source);
22
23
  if (!source) {
23
24
  printResult(io, fail('capabilities.map', 'UNSUPPORTED_CAPABILITY_SOURCE', 'Supported capability sources are all, access-repo, and mcp-server', { source: options.source }, ['Rerun with --source all, --source access-repo, or --source mcp-server']), options.json);
@@ -35,7 +36,7 @@ function runCapabilityMap(io, options) {
35
36
  export function getInstalledCapabilityIds(_config) {
36
37
  return [];
37
38
  }
38
- function parseCapabilityMapSource(source) {
39
+ export function parseCapabilityMapSource(source) {
39
40
  if (CAPABILITY_SOURCE_FILTERS.has(source)) {
40
41
  return source;
41
42
  }
@@ -7,7 +7,7 @@ import { planProxyTest } from '../../services/proxy/proxy-service.js';
7
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
- import { setSkillPresence, clearSkillPresence, getSkillPresence, isSkillPresenceMode } from '../../services/skills/skill-presence-service.js';
10
+ import { setSkillPresence, clearSkillPresence, getSkillPresence, isSkillPresenceMode, touchSkillHeartbeat } from '../../services/skills/skill-presence-service.js';
11
11
  import { fail, ok } from '../../shared/result.js';
12
12
  import { addJsonOption, failUnsupportedNonDryRun, getErrorMessage, isArtifactProvider, isArtifactSetupStep, printResult } from '../cli-helpers.js';
13
13
  export function registerCoreAndArtifactCommands(program, io) {
@@ -86,6 +86,36 @@ export function registerCoreAndArtifactCommands(program, io) {
86
86
  const removed = clearSkillPresence();
87
87
  printResult(io, ok('skill.presence:clear', { active: false, removed }), options.json);
88
88
  });
89
+ addJsonOption(skill
90
+ .command('heartbeat')
91
+ .description('Show the heartbeat status of the active Peaks skill')).action((options) => {
92
+ const presence = getSkillPresence();
93
+ if (presence === null) {
94
+ printResult(io, ok('skill.heartbeat', { active: false, heartbeat: 'none' }), options.json);
95
+ return;
96
+ }
97
+ printResult(io, ok('skill.heartbeat', {
98
+ active: true,
99
+ skill: presence.skill,
100
+ gate: presence.gate ?? null,
101
+ lastHeartbeat: presence.lastHeartbeat ?? presence.setAt,
102
+ setAt: presence.setAt
103
+ }), options.json);
104
+ });
105
+ addJsonOption(skill
106
+ .command('heartbeat:touch')
107
+ .description('Update the heartbeat timestamp (called by the LLM each turn to confirm peaks skill context is alive)')).action((options) => {
108
+ const updated = touchSkillHeartbeat();
109
+ if (updated === null) {
110
+ printResult(io, ok('skill.heartbeat:touch', { active: false, heartbeat: 'none' }), options.json);
111
+ return;
112
+ }
113
+ printResult(io, ok('skill.heartbeat:touch', {
114
+ active: true,
115
+ skill: updated.skill,
116
+ lastHeartbeat: updated.lastHeartbeat
117
+ }), options.json);
118
+ });
89
119
  const profile = program.command('profile').description('Manage runtime profiles');
90
120
  addJsonOption(profile.command('list').description('List available profiles')).action((options) => {
91
121
  printResult(io, ok('profile.list', { profiles: listProfiles() }), options.json);
@@ -10,6 +10,7 @@ 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 { verifyPipeline } from '../../services/workflow/pipeline-verify-service.js';
13
14
  import { fail, ok } from '../../shared/result.js';
14
15
  import { addJsonOption, failUnsupportedNonDryRun, getErrorMessage, isRecommendationWorkflow, printResult } from '../cli-helpers.js';
15
16
  function getCurrentWorkspaceContext() {
@@ -303,6 +304,31 @@ export function registerWorkflowCommands(program, io) {
303
304
  addAutonomousResumeInitOptions(autonomousResume.command('init')).action((options) => runAutonomousResumeInit(io, options));
304
305
  const autonomousResumeAlias = program.command('autonomous-resume').description('Manage autonomous workflow resume artifacts');
305
306
  addAutonomousResumeInitOptions(autonomousResumeAlias.command('init')).action((options) => runAutonomousResumeInit(io, options));
307
+ addJsonOption(workflow
308
+ .command('verify-pipeline')
309
+ .description('Verify the complete rd→qa pipeline was followed for a request')
310
+ .requiredOption('--rid <rid>', 'request identifier')
311
+ .requiredOption('--project <path>', 'project root path')
312
+ .requiredOption('--session-id <id>', 'session identifier')
313
+ .option('--type <type>', 'request type: feature, bugfix, refactor, docs, config, chore', 'feature')).action(async (options) => {
314
+ try {
315
+ const result = await verifyPipeline({
316
+ projectRoot: options.project,
317
+ rid: options.rid,
318
+ sessionId: options.sessionId,
319
+ ...(options.type ? { requestType: options.type } : {})
320
+ });
321
+ const exitOk = result.complete ? 0 : 1;
322
+ printResult(io, result.complete
323
+ ? ok('workflow.verify-pipeline', result)
324
+ : fail('workflow.verify-pipeline', 'PIPELINE_INCOMPLETE', `${result.violations.length} violation(s): ${result.violations.join('; ')}`, result, result.nextActions), options.json);
325
+ process.exitCode = exitOk;
326
+ }
327
+ catch (error) {
328
+ printResult(io, fail('workflow.verify-pipeline', 'VERIFY_FAILED', getErrorMessage(error), {}, ['Check that --project, --rid, and --session-id are correct']), options.json);
329
+ process.exitCode = 1;
330
+ }
331
+ });
306
332
  const swarm = program.command('swarm').description('Plan RD swarm dry-run graphs');
307
333
  addSwarmPlanOptions(swarm.command('plan'), true).action((options) => runSwarmPlan(io, options));
308
334
  addSwarmPlanOptions(program.command('swarm-plan'), false).action((options) => runSwarmPlan(io, options));
@@ -20,7 +20,7 @@ export type SyncResult = {
20
20
  error?: string;
21
21
  };
22
22
  export declare function getLocalArtifactPath(workspace: WorkspaceConfig): string;
23
- export declare function isArtifactWorkspaceOutsideTarget(workspace: WorkspaceConfig, artifactWorkspacePath?: string): boolean;
23
+ export declare function isArtifactWorkspaceOutsideTarget(_workspace: WorkspaceConfig, _artifactWorkspacePath?: string): boolean;
24
24
  export declare function hasValidArtifactWorkspace(workspace: WorkspaceConfig, artifactWorkspacePath?: string): boolean;
25
25
  export declare function getArtifactRemoteRepo(workspace: WorkspaceConfig): WorkspaceConfig['artifactRepo'] | null;
26
26
  export declare function executeArtifactSync(workspaceId?: string): Promise<SyncResult>;
@@ -17,10 +17,8 @@ export function getLocalArtifactPath(workspace) {
17
17
  }
18
18
  return resolve(workspace.rootPath, '.peaks', 'artifacts');
19
19
  }
20
- export function isArtifactWorkspaceOutsideTarget(workspace, artifactWorkspacePath = getLocalArtifactPath(workspace)) {
21
- const targetRoot = canonicalPath(workspace.rootPath);
22
- const artifactRoot = canonicalPath(artifactWorkspacePath);
23
- return !isInsidePath(artifactRoot, targetRoot);
20
+ export function isArtifactWorkspaceOutsideTarget(_workspace, _artifactWorkspacePath) {
21
+ return true;
24
22
  }
25
23
  export function hasValidArtifactWorkspace(workspace, artifactWorkspacePath = getLocalArtifactPath(workspace)) {
26
24
  if (!isArtifactWorkspaceOutsideTarget(workspace, artifactWorkspacePath))
@@ -29,7 +27,6 @@ export function hasValidArtifactWorkspace(workspace, artifactWorkspacePath = get
29
27
  const peaksRoot = canonicalChildPath(artifactWorkspacePath, '.peaks');
30
28
  const changesRoot = canonicalChildPath(artifactWorkspacePath, '.peaks', 'changes');
31
29
  const configPath = canonicalChildPath(artifactWorkspacePath, '.peaks', 'config.json');
32
- const targetRoot = canonicalPath(workspace.rootPath);
33
30
  if (!existsSync(resolve(artifactWorkspacePath, '.peaks', 'config.json')))
34
31
  return false;
35
32
  if (!isInsidePath(peaksRoot, artifactRoot))
@@ -38,12 +35,6 @@ export function hasValidArtifactWorkspace(workspace, artifactWorkspacePath = get
38
35
  return false;
39
36
  if (!isInsidePath(configPath, artifactRoot))
40
37
  return false;
41
- if (isInsidePath(peaksRoot, targetRoot))
42
- return false;
43
- if (isInsidePath(changesRoot, targetRoot))
44
- return false;
45
- if (isInsidePath(configPath, targetRoot))
46
- return false;
47
38
  return true;
48
39
  }
49
40
  export function getArtifactRemoteRepo(workspace) {
@@ -6,7 +6,7 @@ export declare function getProjectConfigPath(projectRoot: string | null): string
6
6
  export declare function getProjectBootstrapConfigPath(projectRoot: string): string;
7
7
  export declare function validateProjectBootstrapConfigPathForWrite(projectRoot: string, configPath: string): void;
8
8
  export declare function validateUserConfigPathForWrite(configPath: string): void;
9
- export declare function validateArtifactWorkspaceRoot(artifactRoot: string, workspaceRoot: string): void;
9
+ export declare function validateArtifactWorkspaceRoot(artifactRoot: string, _workspaceRoot: string): void;
10
10
  export declare function validateArtifactWorkspaceMarkerPath(artifactRoot: string, peaksPath: string, markerPath: string): void;
11
11
  export declare function readConfigFileSafely(configPath: string, errorMessage: string): string;
12
12
  export declare function writeConfigFileSafely(configPath: string, content: string, validateBeforeWrite: () => void, errorMessage: string): void;
@@ -54,6 +54,9 @@ export function findProjectRoot(startPath) {
54
54
  if (existsSync(resolve(current, '.peaks', 'config.json')) && isSafeProjectConfigMarker(current)) {
55
55
  return current;
56
56
  }
57
+ if (existsSync(resolve(current, 'package.json')) || existsSync(resolve(current, '.git'))) {
58
+ return current;
59
+ }
57
60
  parent = current;
58
61
  current = dirname(parent);
59
62
  }
@@ -154,16 +157,11 @@ export function validateUserConfigPathForWrite(configPath) {
154
157
  }
155
158
  }
156
159
  }
157
- export function validateArtifactWorkspaceRoot(artifactRoot, workspaceRoot) {
160
+ export function validateArtifactWorkspaceRoot(artifactRoot, _workspaceRoot) {
158
161
  const artifactStats = lstatSync(artifactRoot);
159
162
  if (!artifactStats.isDirectory() || artifactStats.isSymbolicLink()) {
160
163
  throw new Error('Artifact workspace marker must stay inside the artifact workspace');
161
164
  }
162
- const artifactRootReal = realpathSync(artifactRoot);
163
- const workspaceRootReal = realpathSync(workspaceRoot);
164
- if (isInsidePath(artifactRootReal, workspaceRootReal)) {
165
- throw new Error('Artifact workspace must stay outside the project root');
166
- }
167
165
  }
168
166
  export function validateArtifactWorkspaceMarkerPath(artifactRoot, peaksPath, markerPath) {
169
167
  const artifactStats = lstatSync(artifactRoot);
@@ -23,7 +23,7 @@ export declare function readConfig(projectRoot?: string | null): PeaksConfig;
23
23
  export declare function writeConfig(partial: Partial<PeaksConfig>, layer?: ConfigLayer): void;
24
24
  export declare function getConfig(options?: ConfigGetOptions): unknown;
25
25
  export declare function setConfig(options: ConfigSetOptions): void;
26
- export declare function getWorkspaceConfig(workspaceId: string, projectRoot?: string | null): WorkspaceConfig | null;
26
+ export declare function getWorkspaceConfig(workspaceId: string, _projectRoot?: string | null): WorkspaceConfig | null;
27
27
  export declare function addWorkspace(workspace: WorkspaceConfig, layer?: ConfigLayer): void;
28
28
  export declare function removeWorkspace(workspaceId: string, layer?: ConfigLayer): boolean;
29
29
  export declare function setCurrentWorkspace(workspaceId: string, layer?: ConfigLayer): boolean;
@@ -530,8 +530,21 @@ function writeRawWorkspaceData(data, layer) {
530
530
  writeUserConfigFile(targetPath, content);
531
531
  }
532
532
  }
533
- export function getWorkspaceConfig(workspaceId, projectRoot) {
534
- const { workspaces } = readRawWorkspaceData('user');
533
+ function readAllWorkspaces() {
534
+ const userData = readRawWorkspaceData('user');
535
+ const projectData = readRawWorkspaceData('project');
536
+ const mergedWorkspaces = new Map();
537
+ for (const w of userData.workspaces)
538
+ mergedWorkspaces.set(w.workspaceId, w);
539
+ for (const w of projectData.workspaces)
540
+ mergedWorkspaces.set(w.workspaceId, w);
541
+ return {
542
+ currentWorkspace: projectData.currentWorkspace ?? userData.currentWorkspace,
543
+ workspaces: [...mergedWorkspaces.values()]
544
+ };
545
+ }
546
+ export function getWorkspaceConfig(workspaceId, _projectRoot) {
547
+ const { workspaces } = readAllWorkspaces();
535
548
  return workspaces.find((w) => w.workspaceId === workspaceId) ?? null;
536
549
  }
537
550
  function readLayerConfig(layer) {
@@ -574,13 +587,13 @@ export function setCurrentWorkspace(workspaceId, layer = 'user') {
574
587
  return true;
575
588
  }
576
589
  export function getCurrentWorkspaceConfig() {
577
- const { currentWorkspace, workspaces } = readRawWorkspaceData('user');
590
+ const { currentWorkspace, workspaces } = readAllWorkspaces();
578
591
  if (!currentWorkspace)
579
592
  return null;
580
593
  return workspaces.find((w) => w.workspaceId === currentWorkspace) ?? null;
581
594
  }
582
595
  export function getWorkspaceConfigForPath(path = process.cwd()) {
583
- const { workspaces } = readRawWorkspaceData('user');
596
+ const { workspaces } = readAllWorkspaces();
584
597
  return findWorkspaceForPath(workspaces, path);
585
598
  }
586
599
  function findWorkspaceForPath(workspaces, path) {
@@ -9,16 +9,13 @@ function extractAcceptanceItems(prdBody) {
9
9
  return [];
10
10
  }
11
11
  // Find the line where the header starts.
12
- let headerLine = -1;
12
+ let headerLine = 0;
13
13
  for (let i = 0; i < lines.length; i += 1) {
14
14
  if (ACCEPTANCE_SECTION_PATTERN.test((lines[i] ?? '') + '\n')) {
15
15
  headerLine = i;
16
16
  break;
17
17
  }
18
18
  }
19
- if (headerLine === -1) {
20
- return [];
21
- }
22
19
  const items = [];
23
20
  let counter = 0;
24
21
  for (let i = headerLine + 1; i < lines.length; i += 1) {
@@ -101,13 +101,7 @@ async function countSrcFiles(projectRoot, max = 500) {
101
101
  const current = queue.shift();
102
102
  if (current === undefined)
103
103
  break;
104
- let entries;
105
- try {
106
- entries = await readdir(current, { withFileTypes: true });
107
- }
108
- catch {
109
- continue;
110
- }
104
+ const entries = await readdir(current, { withFileTypes: true });
111
105
  for (const entry of entries) {
112
106
  if (entry.name.startsWith('.') || entry.name === 'node_modules')
113
107
  continue;
@@ -129,14 +123,9 @@ async function lockfileAgeDays(projectRoot) {
129
123
  for (const candidate of candidates) {
130
124
  const full = join(projectRoot, candidate);
131
125
  if (await pathExists(full)) {
132
- try {
133
- const stats = await stat(full);
134
- const ageMs = Date.now() - stats.mtimeMs;
135
- return Math.floor(ageMs / (1000 * 60 * 60 * 24));
136
- }
137
- catch {
138
- return null;
139
- }
126
+ const stats = await stat(full);
127
+ const ageMs = Date.now() - stats.mtimeMs;
128
+ return Math.floor(ageMs / (1000 * 60 * 60 * 24));
140
129
  }
141
130
  }
142
131
  return null;
@@ -57,9 +57,9 @@ export function globToRegex(pattern) {
57
57
  }
58
58
  // If the pattern ends with no trailing slash and no extension wildcard, also allow it to match files under the path (treat as dir prefix)
59
59
  // E.g. `src/services/login` should match `src/services/login/handler.ts`.
60
- if (!trimmed.includes('*') && !trimmed.includes('?') && !trimmed.includes('.')) {
61
- body = `${body}(?:/.*)?`;
62
- }
60
+ body = (!trimmed.includes("*") && !trimmed.includes("?") && !trimmed.includes("."))
61
+ ? `${body}(?:/.*)?`
62
+ : body;
63
63
  return new RegExp(`^${body}$`);
64
64
  }
65
65
  function classifyPatternLine(raw) {
@@ -15,13 +15,8 @@ function getChangedFiles(projectRoot, baseRef) {
15
15
  }
16
16
  }
17
17
  function countLines(filePath) {
18
- try {
19
- const content = readFileSync(filePath, 'utf8');
20
- return content.split(/\r?\n/).length;
21
- }
22
- catch {
23
- return 0;
24
- }
18
+ const content = readFileSync(filePath, 'utf8');
19
+ return content.split(/\r?\n/).length;
25
20
  }
26
21
  export function scanFileSize(options) {
27
22
  const baseRef = options.baseRef ?? 'HEAD';
@@ -78,9 +78,7 @@ function isConsistent(declared, suggested) {
78
78
  return suggested.includes(declared);
79
79
  }
80
80
  function buildRationale(declared, breakdown, suggested, consistent) {
81
- const summary = breakdown.length === 0
82
- ? 'no changed files detected'
83
- : breakdown.map((entry) => `${entry.category}=${entry.count}`).join(', ');
81
+ const summary = breakdown.map((entry) => `${entry.category}=${entry.count}`).join(', ');
84
82
  if (consistent) {
85
83
  return `declared --type=${declared} is consistent with the changed files (${summary})`;
86
84
  }
@@ -6,8 +6,10 @@ export type SkillPresence = {
6
6
  mode?: SkillPresenceMode;
7
7
  gate?: string;
8
8
  setAt: string;
9
+ lastHeartbeat?: string;
9
10
  };
10
11
  export declare function exportSkillPresence(): string;
11
12
  export declare function setSkillPresence(skill: string, mode?: string, gate?: string): SkillPresence;
12
13
  export declare function getSkillPresence(): SkillPresence | null;
14
+ export declare function touchSkillHeartbeat(): SkillPresence | null;
13
15
  export declare function clearSkillPresence(): boolean;
@@ -18,11 +18,13 @@ export function exportSkillPresence() {
18
18
  }
19
19
  export function setSkillPresence(skill, mode, gate) {
20
20
  const validatedMode = mode && isSkillPresenceMode(mode) ? mode : undefined;
21
+ const now = new Date().toISOString();
21
22
  const presence = {
22
23
  skill,
23
24
  ...(validatedMode ? { mode: validatedMode } : {}),
24
25
  ...(gate ? { gate } : {}),
25
- setAt: new Date().toISOString()
26
+ setAt: now,
27
+ lastHeartbeat: now
26
28
  };
27
29
  const presencePath = resolvePresencePath();
28
30
  const presenceDir = dirname(presencePath);
@@ -49,6 +51,25 @@ export function getSkillPresence() {
49
51
  return null;
50
52
  }
51
53
  }
54
+ export function touchSkillHeartbeat() {
55
+ const presencePath = resolvePresencePath();
56
+ if (!existsSync(presencePath)) {
57
+ return null;
58
+ }
59
+ try {
60
+ const raw = readFileSync(presencePath, 'utf8');
61
+ const parsed = JSON.parse(raw);
62
+ if (typeof parsed?.skill !== 'string' || parsed.skill.length === 0) {
63
+ return null;
64
+ }
65
+ parsed.lastHeartbeat = new Date().toISOString();
66
+ writeFileSync(presencePath, JSON.stringify(parsed, null, 2), 'utf8');
67
+ return parsed;
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
52
73
  export function clearSkillPresence() {
53
74
  const presencePath = resolvePresencePath();
54
75
  if (!existsSync(presencePath)) {
@@ -134,6 +134,27 @@ function renderClaudeMd(language, ctx) {
134
134
  '- Read `.claude/rules/common/security.md` before touching filesystem, user input, external calls, auth, or secrets.',
135
135
  `- Read .claude/rules/${language}/coding-style.md for language-specific standards when applicable.`,
136
136
  '',
137
+ '<!--',
138
+ ' Peaks-Cli 心跳检测 (heartbeat check)',
139
+ ' LLM 每轮回复前必须执行以下步骤,由 CLI 命令强制执行:',
140
+ ' 1. 读取 .peaks/.active-skill.json → 存在则显示 status header',
141
+ ' 2. 扫描当前上下文查找 peaks 技能标记',
142
+ ' ("Peaks-Cli Gate", "Peaks-Cli Default runbook", "Peaks-Cli Startup sequence")',
143
+ ' 3. 标记存在 → 调用 `peaks skill heartbeat:touch` 记录心跳',
144
+ ' 4. 标记不存在 → 显示警告 + 调用 `peaks skill presence:clear` 关闭心跳',
145
+ '-->',
146
+ 'Active Peaks-Cli skill presence: at the start of EVERY response, run these steps in order:',
147
+ '',
148
+ '1. Read `.peaks/.active-skill.json`. If missing or invalid, skip all remaining steps.',
149
+ '2. Display the compact status header: `Peaks-Cli Skill: <skill> | Peaks-Cli Gate: <gate> | Next: <one short action>`.',
150
+ '3. Scan the current conversation context for peaks skill markers: "Peaks-Cli Gate", "Peaks-Cli Default runbook", "Peaks-Cli Startup sequence", "Peaks-Cli Swarm parallel phase".',
151
+ '4. If at least one marker IS found → run `peaks skill heartbeat:touch` to record a successful heartbeat. The CLI command writes the timestamp and returns JSON — verify it succeeded.',
152
+ '5. If NO markers are found → the skill context has been lost (context compaction). Run `peaks skill presence:clear` to close the heartbeat, then display this warning:',
153
+ '',
154
+ '> ⚠ Peaks-Cli: skill presence file was active but skill context has been lost from the conversation. The workflow can no longer run correctly. Please re-invoke the relevant /peaks-* skill to reload the full skill instructions.',
155
+ '',
156
+ 'Do NOT skip step 3-5. The CLI heartbeat:touch command is the mechanism that makes heartbeat auditable — failing to call it means the heartbeat is broken.',
157
+ '',
137
158
  'External reference: https://github.com/affaan-m/everything-claude-code is used as a curated reference only. Do not execute or install external content without explicit approval.',
138
159
  ''
139
160
  ].join('\n');
@@ -0,0 +1,31 @@
1
+ import type { RequestType } from '../artifacts/artifact-prerequisites.js';
2
+ export type PipelineGate = {
3
+ name: string;
4
+ description: string;
5
+ passed: boolean;
6
+ detail: string;
7
+ };
8
+ export type PipelineVerification = {
9
+ rid: string;
10
+ sessionId: string;
11
+ requestType: RequestType;
12
+ complete: boolean;
13
+ rdPhase: {
14
+ invoked: boolean;
15
+ state: string;
16
+ gates: PipelineGate[];
17
+ };
18
+ qaPhase: {
19
+ invoked: boolean;
20
+ state: string;
21
+ gates: PipelineGate[];
22
+ };
23
+ violations: string[];
24
+ nextActions: string[];
25
+ };
26
+ export declare function verifyPipeline(options: {
27
+ projectRoot: string;
28
+ rid: string;
29
+ sessionId: string;
30
+ requestType?: string;
31
+ }): Promise<PipelineVerification>;