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.
- package/bin/peaks.js +0 -0
- package/dist/src/cli/commands/capability-commands.d.ts +11 -0
- package/dist/src/cli/commands/capability-commands.js +7 -6
- package/dist/src/cli/commands/core-artifact-commands.js +31 -1
- package/dist/src/cli/commands/workflow-commands.js +26 -0
- package/dist/src/services/artifacts/workspace-service.d.ts +1 -1
- package/dist/src/services/artifacts/workspace-service.js +2 -11
- package/dist/src/services/config/config-safety.d.ts +1 -1
- package/dist/src/services/config/config-safety.js +4 -6
- package/dist/src/services/config/config-service.d.ts +1 -1
- package/dist/src/services/config/config-service.js +17 -4
- package/dist/src/services/scan/acceptance-coverage-service.js +1 -4
- package/dist/src/services/scan/archetype-service.js +4 -15
- package/dist/src/services/scan/diff-scope-service.js +3 -3
- package/dist/src/services/scan/file-size-scan.js +2 -7
- package/dist/src/services/scan/type-sanity-service.js +1 -3
- package/dist/src/services/skills/skill-presence-service.d.ts +2 -0
- package/dist/src/services/skills/skill-presence-service.js +22 -1
- package/dist/src/services/standards/project-standards-service.js +21 -0
- package/dist/src/services/workflow/pipeline-verify-service.d.ts +31 -0
- package/dist/src/services/workflow/pipeline-verify-service.js +180 -0
- package/dist/src/shared/change-id.js +0 -3
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/output-styles/peaks-skill-swarm.md +34 -34
- package/package.json +2 -1
- package/skills/peaks-prd/SKILL.md +10 -10
- package/skills/peaks-qa/SKILL.md +48 -36
- package/skills/peaks-rd/SKILL.md +53 -53
- package/skills/peaks-sc/SKILL.md +9 -9
- package/skills/peaks-solo/SKILL.md +127 -91
- package/skills/peaks-txt/SKILL.md +17 -17
- 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(
|
|
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(
|
|
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(
|
|
21
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
534
|
-
const
|
|
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 } =
|
|
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 } =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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.
|
|
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:
|
|
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>;
|