peaks-cli 1.0.14 → 1.0.15

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.
@@ -1,280 +1,11 @@
1
- import { closeSync, constants, existsSync, fchmodSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
- import { randomUUID } from 'node:crypto';
3
- import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
1
+ import { existsSync, mkdirSync } from 'node:fs';
4
2
  import { homedir } from 'node:os';
3
+ import { basename, dirname, isAbsolute, resolve } from 'node:path';
5
4
  import { DEFAULT_CONFIG } from './config-types.js';
6
5
  import { stablePath } from '../../shared/path-utils.js';
7
- function getUserConfigPath() {
8
- return resolve(homedir(), '.peaks', 'config.json');
9
- }
10
- function isInsidePath(childPath, parentPath) {
11
- const relativePath = relative(parentPath, childPath);
12
- return relativePath === '' || (!relativePath.startsWith('..') && !isAbsolute(relativePath));
13
- }
14
- function isSafeProjectConfigMarker(projectRoot) {
15
- const peaksPath = resolve(projectRoot, '.peaks');
16
- const markerPath = resolve(peaksPath, 'config.json');
17
- try {
18
- const projectRootReal = realpathSync(projectRoot);
19
- const peaksStats = lstatSync(peaksPath);
20
- const peaksReal = realpathSync(peaksPath);
21
- const markerStats = lstatSync(markerPath);
22
- if (!peaksStats.isDirectory() || peaksStats.isSymbolicLink())
23
- return false;
24
- if (!markerStats.isFile() || markerStats.isSymbolicLink() || markerStats.nlink !== 1)
25
- return false;
26
- const markerReal = realpathSync(markerPath);
27
- if (!isInsidePath(peaksReal, projectRootReal))
28
- return false;
29
- if (!isInsidePath(markerReal, projectRootReal))
30
- return false;
31
- return isInsidePath(markerReal, peaksReal);
32
- }
33
- catch {
34
- return false;
35
- }
36
- }
37
- function normalizeBoundaryPath(path) {
38
- const resolved = resolve(path);
39
- let realPath = resolved;
40
- try {
41
- realPath = existsSync(resolved) ? realpathSync.native(resolved) : resolved;
42
- }
43
- catch {
44
- realPath = resolved;
45
- }
46
- return process.platform === 'win32' || process.platform === 'darwin' ? realPath.toLowerCase() : realPath;
47
- }
48
- function getHomeBoundaryPaths() {
49
- return new Set([homedir(), process.env.HOME, process.env.USERPROFILE].filter((path) => typeof path === 'string' && path.length > 0).map(normalizeBoundaryPath));
50
- }
51
- function findProjectRoot(startPath) {
52
- const homeBoundaryPaths = getHomeBoundaryPaths();
53
- let current = resolve(startPath);
54
- let parent = dirname(current);
55
- while (current !== parent && !homeBoundaryPaths.has(normalizeBoundaryPath(current))) {
56
- if (existsSync(resolve(current, '.peaks', 'config.json')) && isSafeProjectConfigMarker(current)) {
57
- return current;
58
- }
59
- parent = current;
60
- current = dirname(parent);
61
- }
62
- return null;
63
- }
64
- export function resolveProjectRootForConfig(startPath) {
65
- const start = resolve(startPath);
66
- const homeBoundaryPaths = getHomeBoundaryPaths();
67
- let current = start;
68
- let parent = dirname(current);
69
- while (current !== parent && !homeBoundaryPaths.has(normalizeBoundaryPath(current))) {
70
- if (existsSync(resolve(current, '.peaks', 'config.json')) && isSafeProjectConfigMarker(current)) {
71
- return current;
72
- }
73
- if (existsSync(resolve(current, 'package.json')) || existsSync(resolve(current, '.git'))) {
74
- return current;
75
- }
76
- parent = current;
77
- current = dirname(parent);
78
- }
79
- return start;
80
- }
81
- function getProjectConfigPath(projectRoot) {
82
- if (!projectRoot)
83
- return null;
84
- if (!isSafeProjectConfigMarker(projectRoot))
85
- return null;
86
- return resolve(projectRoot, '.peaks', 'config.json');
87
- }
88
- function getProjectBootstrapConfigPath(projectRoot) {
89
- const projectRootPath = resolve(projectRoot);
90
- const peaksPath = resolve(projectRootPath, '.peaks');
91
- const configPath = resolve(peaksPath, 'config.json');
92
- if (!isInsidePath(configPath, projectRootPath)) {
93
- throw new Error('Project config path must stay inside the project root');
94
- }
95
- if (!existsSync(peaksPath)) {
96
- mkdirSync(peaksPath, { recursive: true });
97
- }
98
- validateProjectBootstrapConfigPath(projectRootPath, peaksPath, configPath);
99
- return configPath;
100
- }
101
- function validateProjectBootstrapConfigPath(projectRootPath, peaksPath, configPath) {
102
- const projectRootReal = realpathSync(projectRootPath);
103
- const peaksStats = lstatSync(peaksPath);
104
- const peaksReal = realpathSync(peaksPath);
105
- if (!peaksStats.isDirectory() || peaksStats.isSymbolicLink() || peaksReal !== resolve(projectRootReal, '.peaks')) {
106
- throw new Error('Project config path must stay inside the project root');
107
- }
108
- try {
109
- const markerStats = lstatSync(configPath);
110
- if (!markerStats.isFile() || markerStats.isSymbolicLink()) {
111
- throw new Error('Project config path must stay inside the project root');
112
- }
113
- if (markerStats.nlink !== 1) {
114
- throw new Error('Config path must not be hardlinked');
115
- }
116
- const markerReal = realpathSync(configPath);
117
- if (!isInsidePath(markerReal, projectRootReal) || !isInsidePath(markerReal, peaksReal)) {
118
- throw new Error('Project config path must stay inside the project root');
119
- }
120
- }
121
- catch (error) {
122
- if (error.code !== 'ENOENT') {
123
- throw error;
124
- }
125
- }
126
- }
127
- function validateProjectBootstrapConfigPathForWrite(projectRoot, configPath) {
128
- const projectRootPath = resolve(projectRoot);
129
- validateProjectBootstrapConfigPath(projectRootPath, resolve(projectRootPath, '.peaks'), configPath);
130
- }
131
- function validateUserConfigPathForWrite(configPath) {
132
- const userRoot = resolve(homedir());
133
- const peaksPath = resolve(userRoot, '.peaks');
134
- const userRootReal = realpathSync(userRoot);
135
- const peaksStats = lstatSync(peaksPath);
136
- const peaksReal = realpathSync(peaksPath);
137
- if (!peaksStats.isDirectory() || peaksStats.isSymbolicLink() || peaksReal !== resolve(userRootReal, '.peaks')) {
138
- throw new Error('User config path must stay inside the user root');
139
- }
140
- try {
141
- const markerStats = lstatSync(configPath);
142
- if (!markerStats.isFile() || markerStats.isSymbolicLink()) {
143
- throw new Error('User config path must stay inside the user root');
144
- }
145
- if (markerStats.nlink !== 1) {
146
- throw new Error('Config path must not be hardlinked');
147
- }
148
- const markerReal = realpathSync(configPath);
149
- if (!isInsidePath(markerReal, userRootReal) || !isInsidePath(markerReal, peaksReal)) {
150
- throw new Error('User config path must stay inside the user root');
151
- }
152
- }
153
- catch (error) {
154
- if (error.code !== 'ENOENT') {
155
- throw error;
156
- }
157
- }
158
- }
159
- function validateArtifactWorkspaceRoot(artifactRoot, workspaceRoot) {
160
- const artifactStats = lstatSync(artifactRoot);
161
- if (!artifactStats.isDirectory() || artifactStats.isSymbolicLink()) {
162
- throw new Error('Artifact workspace marker must stay inside the artifact workspace');
163
- }
164
- const artifactRootReal = realpathSync(artifactRoot);
165
- const workspaceRootReal = realpathSync(workspaceRoot);
166
- if (isInsidePath(artifactRootReal, workspaceRootReal)) {
167
- throw new Error('Artifact workspace must stay outside the project root');
168
- }
169
- }
170
- function validateArtifactWorkspaceMarkerPath(artifactRoot, peaksPath, markerPath) {
171
- const artifactStats = lstatSync(artifactRoot);
172
- if (!artifactStats.isDirectory() || artifactStats.isSymbolicLink()) {
173
- throw new Error('Artifact workspace marker must stay inside the artifact workspace');
174
- }
175
- const artifactRootReal = realpathSync(artifactRoot);
176
- const peaksStats = lstatSync(peaksPath);
177
- const peaksReal = realpathSync(peaksPath);
178
- if (!peaksStats.isDirectory() || peaksStats.isSymbolicLink() || peaksReal !== resolve(artifactRootReal, '.peaks')) {
179
- throw new Error('Artifact workspace marker must stay inside the artifact workspace');
180
- }
181
- try {
182
- const markerStats = lstatSync(markerPath);
183
- if (!markerStats.isFile() || markerStats.isSymbolicLink()) {
184
- throw new Error('Artifact workspace marker must stay inside the artifact workspace');
185
- }
186
- if (markerStats.nlink !== 1) {
187
- throw new Error('Config path must not be hardlinked');
188
- }
189
- const markerReal = realpathSync(markerPath);
190
- if (!isInsidePath(markerReal, artifactRootReal) || !isInsidePath(markerReal, peaksReal)) {
191
- throw new Error('Artifact workspace marker must stay inside the artifact workspace');
192
- }
193
- }
194
- catch (error) {
195
- if (error.code !== 'ENOENT') {
196
- throw error;
197
- }
198
- }
199
- }
200
- function validateOpenConfigFile(fd, tempPath, errorMessage) {
201
- const fdStats = fstatSync(fd);
202
- const pathStats = lstatSync(tempPath);
203
- if (!fdStats.isFile() || !pathStats.isFile() || fdStats.dev !== pathStats.dev || fdStats.ino !== pathStats.ino) {
204
- throw new Error(errorMessage);
205
- }
206
- if (fdStats.nlink !== 1 || pathStats.nlink !== 1) {
207
- throw new Error('Config path must not be hardlinked');
208
- }
209
- }
210
- function getSafeTempOpenFlags() {
211
- const baseFlags = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL;
212
- return typeof constants.O_NOFOLLOW === 'number' ? baseFlags | constants.O_NOFOLLOW : baseFlags;
213
- }
214
- function getSafeReadOpenFlags() {
215
- return typeof constants.O_NOFOLLOW === 'number' ? constants.O_RDONLY | constants.O_NOFOLLOW : constants.O_RDONLY;
216
- }
217
- function readConfigFileSafely(configPath, errorMessage) {
218
- const fd = openSync(configPath, getSafeReadOpenFlags());
219
- try {
220
- validateOpenConfigFile(fd, configPath, errorMessage);
221
- return readFileSync(fd, 'utf-8');
222
- }
223
- finally {
224
- closeSync(fd);
225
- }
226
- }
227
- function writeConfigFileSafely(configPath, content, validateBeforeWrite, errorMessage) {
228
- validateBeforeWrite();
229
- const tempPath = `${configPath}.${process.pid}.${randomUUID()}.tmp`;
230
- let fd = openSync(tempPath, getSafeTempOpenFlags(), 0o600);
231
- let renamed = false;
232
- let closeError;
233
- try {
234
- validateOpenConfigFile(fd, tempPath, errorMessage);
235
- fchmodSync(fd, 0o600);
236
- writeFileSync(fd, content, 'utf-8');
237
- const writeFd = fd;
238
- fd = null;
239
- closeSync(writeFd);
240
- validateBeforeWrite();
241
- const readFd = openSync(tempPath, getSafeReadOpenFlags());
242
- try {
243
- validateOpenConfigFile(readFd, tempPath, errorMessage);
244
- }
245
- finally {
246
- closeSync(readFd);
247
- }
248
- renameSync(tempPath, configPath);
249
- renamed = true;
250
- }
251
- finally {
252
- if (fd !== null) {
253
- try {
254
- closeSync(fd);
255
- }
256
- catch (error) {
257
- closeError = error;
258
- }
259
- }
260
- try {
261
- if (!renamed && existsSync(tempPath)) {
262
- unlinkSync(tempPath);
263
- }
264
- }
265
- finally {
266
- if (closeError) {
267
- throw closeError;
268
- }
269
- }
270
- }
271
- }
272
- function writeProjectConfigFile(projectRoot, configPath, content) {
273
- writeConfigFileSafely(configPath, content, () => validateProjectBootstrapConfigPathForWrite(projectRoot, configPath), 'Project config path must stay inside the project root');
274
- }
275
- function writeUserConfigFile(configPath, content) {
276
- writeConfigFileSafely(configPath, content, () => validateUserConfigPathForWrite(configPath), 'User config path must stay inside the user root');
277
- }
6
+ import { findProjectRoot, getProjectBootstrapConfigPath, getProjectConfigPath, getUserConfigPath, isInsidePath, readConfigFileSafely, resolveProjectRootForConfig, validateArtifactWorkspaceMarkerPath, validateArtifactWorkspaceRoot, validateProjectBootstrapConfigPathForWrite, validateUserConfigPathForWrite, writeConfigFileSafely, writeProjectConfigFile, writeUserConfigFile } from './config-safety.js';
7
+ // Re-export resolveProjectRootForConfig for external consumers
8
+ export { resolveProjectRootForConfig } from './config-safety.js';
278
9
  function readJsonFile(path, validateBeforeRead, errorMessage = 'Config path must stay inside the config root') {
279
10
  if (!path || !existsSync(path))
280
11
  return null;
@@ -2,6 +2,7 @@ import { type RequestArtifactRole, type RequestArtifactSummary } from '../artifa
2
2
  import type { OpenSpecChangeSummary } from '../openspec/openspec-types.js';
3
3
  import type { McpScanReport } from '../mcp/mcp-types.js';
4
4
  import type { CapabilityItem } from '../recommendations/recommendation-types.js';
5
+ import { type SkillPresence } from '../skills/skill-presence-service.js';
5
6
  export type ProjectDashboardRequests = {
6
7
  count: number;
7
8
  byRole: Record<RequestArtifactRole, RequestArtifactSummary[]>;
@@ -39,6 +40,14 @@ export type ProjectDashboardCapabilities = {
39
40
  mcpCount: number;
40
41
  sample: Array<Pick<CapabilityItem, 'capabilityId' | 'name' | 'itemType' | 'category'>>;
41
42
  };
43
+ export type ProjectDashboardSkillPresence = {
44
+ active: boolean;
45
+ fresh: boolean;
46
+ skill?: string;
47
+ mode?: string;
48
+ gate?: string;
49
+ setAt?: string;
50
+ };
42
51
  export type ProjectDashboard = {
43
52
  generatedAt: string;
44
53
  projectRoot: string;
@@ -49,6 +58,7 @@ export type ProjectDashboard = {
49
58
  doctor: ProjectDashboardDoctor;
50
59
  runbookHealth: ProjectDashboardRunbookHealth;
51
60
  capabilities: ProjectDashboardCapabilities;
61
+ skillPresence: ProjectDashboardSkillPresence;
52
62
  };
53
63
  export type LoadProjectDashboardOptions = {
54
64
  projectRoot: string;
@@ -60,5 +70,6 @@ export type LoadProjectDashboardOptions = {
60
70
  failed: number;
61
71
  };
62
72
  runbookHealth?: ProjectDashboardRunbookHealth;
73
+ skillPresence?: SkillPresence | null;
63
74
  };
64
75
  export declare function loadProjectDashboard(options: LoadProjectDashboardOptions): Promise<ProjectDashboard>;
@@ -4,11 +4,13 @@ import { scanMcpServers } from '../mcp/mcp-scan-service.js';
4
4
  import { scanUnderstandAnything } from '../understand/understand-scan-service.js';
5
5
  import { seedCapabilityItems } from '../recommendations/capability-seed-items.js';
6
6
  import { requiredSkillNames } from '../../shared/paths.js';
7
+ import { getSkillPresence } from '../skills/skill-presence-service.js';
8
+ const SKILL_PRESENCE_FRESHNESS_THRESHOLD_MS = 24 * 60 * 60 * 1000;
7
9
  function defaultClock() {
8
10
  return new Date().toISOString();
9
11
  }
10
12
  function groupRequestsByRole(items) {
11
- const byRole = { prd: [], ui: [], rd: [], qa: [] };
13
+ const byRole = { prd: [], ui: [], rd: [], qa: [], sc: [] };
12
14
  for (const item of items) {
13
15
  byRole[item.role].push(item);
14
16
  }
@@ -72,6 +74,22 @@ function buildCapabilitiesSummary(sampleSize) {
72
74
  }))
73
75
  };
74
76
  }
77
+ function buildSkillPresenceSummary(presence) {
78
+ const resolved = presence === undefined ? getSkillPresence() : presence;
79
+ if (resolved === null) {
80
+ return { active: false, fresh: true };
81
+ }
82
+ const setAtMs = Date.parse(resolved.setAt);
83
+ const fresh = !Number.isNaN(setAtMs) && Date.now() - setAtMs <= SKILL_PRESENCE_FRESHNESS_THRESHOLD_MS;
84
+ return {
85
+ active: true,
86
+ fresh,
87
+ skill: resolved.skill,
88
+ ...(resolved.mode !== undefined ? { mode: resolved.mode } : {}),
89
+ ...(resolved.gate !== undefined ? { gate: resolved.gate } : {}),
90
+ setAt: resolved.setAt
91
+ };
92
+ }
75
93
  export async function loadProjectDashboard(options) {
76
94
  const clock = options.clock ?? defaultClock;
77
95
  const sampleSize = options.sampleCapabilities ?? 8;
@@ -107,6 +125,7 @@ export async function loadProjectDashboard(options) {
107
125
  },
108
126
  doctor: doctorAndRunbook.doctor,
109
127
  runbookHealth: doctorAndRunbook.runbookHealth,
110
- capabilities: buildCapabilitiesSummary(sampleSize)
128
+ capabilities: buildCapabilitiesSummary(sampleSize),
129
+ skillPresence: buildSkillPresenceSummary(options.skillPresence)
111
130
  };
112
131
  }
@@ -1,3 +1,4 @@
1
+ import { type SkillPresence } from '../skills/skill-presence-service.js';
1
2
  export type DoctorCheck = {
2
3
  id: string;
3
4
  ok: boolean;
@@ -21,5 +22,7 @@ export type DoctorOptions = {
21
22
  schemasBaseDir?: string;
22
23
  skillsBaseDir?: string;
23
24
  codegraphProbe?: () => CodegraphCapabilityProbe;
25
+ skillPresenceProbe?: () => SkillPresence | null;
26
+ skillPresenceFreshnessThresholdMs?: number;
24
27
  };
25
28
  export declare function runDoctor(options?: DoctorOptions): Promise<DoctorReport>;
@@ -7,7 +7,9 @@ import { readText } from '../../shared/fs.js';
7
7
  import { requiredSchemaFiles, requiredSkillNames, schemasDir } from '../../shared/paths.js';
8
8
  import { getErrorMessage } from '../../shared/result.js';
9
9
  import { loadSkillRegistry } from '../skills/skill-registry.js';
10
+ import { getSkillPresence } from '../skills/skill-presence-service.js';
10
11
  const CODEGRAPH_EXPECTED_VERSION = '0.7.10';
12
+ const SKILL_PRESENCE_FRESHNESS_THRESHOLD_MS = 24 * 60 * 60 * 1000;
11
13
  function defaultCodegraphProbe() {
12
14
  const require = createRequire(import.meta.url);
13
15
  const packagePath = require.resolve('@colbymchenry/codegraph/package.json');
@@ -132,6 +134,62 @@ export async function runDoctor(options = {}) {
132
134
  ok: true,
133
135
  message: hasUserConfig ? 'User config exists at ~/.peaks/config.json' : 'Optional user config not found at ~/.peaks/config.json'
134
136
  });
137
+ const presenceProbe = options.skillPresenceProbe ?? getSkillPresence;
138
+ const freshnessThresholdMs = options.skillPresenceFreshnessThresholdMs ?? SKILL_PRESENCE_FRESHNESS_THRESHOLD_MS;
139
+ let presence = null;
140
+ try {
141
+ presence = presenceProbe();
142
+ }
143
+ catch {
144
+ presence = null;
145
+ }
146
+ if (presence === null) {
147
+ checks.push({
148
+ id: 'skill-presence:current',
149
+ ok: true,
150
+ message: 'No active Peaks skill presence (.peaks/.active-skill.json absent or invalid)'
151
+ });
152
+ checks.push({
153
+ id: 'skill-presence:freshness',
154
+ ok: true,
155
+ message: 'No active Peaks skill presence to age-check'
156
+ });
157
+ }
158
+ else {
159
+ const modePart = presence.mode !== undefined ? `, mode ${presence.mode}` : '';
160
+ const gatePart = presence.gate !== undefined ? `, gate ${presence.gate}` : '';
161
+ checks.push({
162
+ id: 'skill-presence:current',
163
+ ok: true,
164
+ message: `Active Peaks skill presence: ${presence.skill}${modePart}${gatePart} (set ${presence.setAt})`
165
+ });
166
+ const setAtMs = Date.parse(presence.setAt);
167
+ if (Number.isNaN(setAtMs)) {
168
+ checks.push({
169
+ id: 'skill-presence:freshness',
170
+ ok: false,
171
+ message: `Skill presence ${presence.skill} has invalid setAt: ${presence.setAt}`
172
+ });
173
+ }
174
+ else {
175
+ const ageMs = Date.now() - setAtMs;
176
+ if (ageMs > freshnessThresholdMs) {
177
+ const ageHours = Math.round(ageMs / (60 * 60 * 1000));
178
+ checks.push({
179
+ id: 'skill-presence:freshness',
180
+ ok: false,
181
+ message: `Skill presence ${presence.skill} is stale (set ${presence.setAt}, ~${ageHours}h ago); run peaks skill presence:clear if the role has ended`
182
+ });
183
+ }
184
+ else {
185
+ checks.push({
186
+ id: 'skill-presence:freshness',
187
+ ok: true,
188
+ message: `Skill presence ${presence.skill} is fresh (set ${presence.setAt})`
189
+ });
190
+ }
191
+ }
192
+ }
135
193
  const probe = options.codegraphProbe ?? defaultCodegraphProbe;
136
194
  try {
137
195
  const result = probe();
@@ -0,0 +1,16 @@
1
+ export type AutonomousResumeWriteRequest = {
2
+ readonly changeId: string;
3
+ readonly goal: string;
4
+ readonly artifactWorkspacePath: string;
5
+ readonly apply?: boolean;
6
+ readonly clock?: () => string;
7
+ };
8
+ export type AutonomousResumeArtifactFile = {
9
+ readonly path: string;
10
+ readonly content: string;
11
+ };
12
+ export type AutonomousResumeWriteResult = {
13
+ readonly applied: boolean;
14
+ readonly files: readonly AutonomousResumeArtifactFile[];
15
+ };
16
+ export declare function writeAutonomousResumeArtifacts(request: AutonomousResumeWriteRequest): Promise<AutonomousResumeWriteResult>;
@@ -0,0 +1,156 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { buildArtifactRelativePath, validateChangeIdOrThrow } from '../../shared/change-id.js';
4
+ import { pathExists } from '../../shared/fs.js';
5
+ function defaultClock() {
6
+ return new Date().toISOString();
7
+ }
8
+ function normalizeGoal(goal) {
9
+ const trimmed = goal.trim();
10
+ if (!trimmed) {
11
+ throw new Error('Goal must be non-empty');
12
+ }
13
+ return trimmed;
14
+ }
15
+ function renderGoalPackage(changeId, goal) {
16
+ return `${JSON.stringify({
17
+ changeId,
18
+ artifactType: 'goal-package',
19
+ status: 'ready',
20
+ goal,
21
+ doneCondition: `Autonomous plan for ${changeId} is complete when all acceptance criteria pass, the worker queue is empty or blocked with next actions, and validation evidence is recorded.`,
22
+ resumeCondition: `Resume ${changeId} only after checkpoint artifacts, worker queue state, and validation evidence requirements have been verified.`,
23
+ acceptanceCriteria: [
24
+ 'A resumable autonomous RD plan exists with checkpoints, worker queue, and validation evidence requirements.',
25
+ 'Curated capabilities from docs/accessRepo.md and docs/mcpServer.md are considered before custom implementation.',
26
+ 'Resume after compact verifies checkpoints and evidence before continuing.',
27
+ 'All execution remains dry-run until explicitly approved.'
28
+ ]
29
+ }, null, 2)}\n`;
30
+ }
31
+ function renderRdPlan(changeId) {
32
+ return `${JSON.stringify({
33
+ changeId,
34
+ artifactType: 'rd-plan',
35
+ status: 'ready',
36
+ workerQueueStatus: 'ready',
37
+ taskCount: 1,
38
+ reducerRequired: true
39
+ }, null, 2)}\n`;
40
+ }
41
+ function renderCheckpoint(changeId, createdAt) {
42
+ return `${JSON.stringify({
43
+ changeId,
44
+ artifactType: 'checkpoint',
45
+ status: 'ready',
46
+ checkpointId: 'checkpoint-1',
47
+ createdAt,
48
+ workerQueueState: {},
49
+ validationRefs: ['unit-tests.md']
50
+ }, null, 2)}\n`;
51
+ }
52
+ function renderValidationReport(changeId) {
53
+ return `---
54
+ changeId: ${changeId}
55
+ artifactType: validation-report
56
+ status: passed
57
+ ---
58
+
59
+ Validation summary:
60
+
61
+ - Resume artifact scaffold generated for ${changeId}.
62
+
63
+ Checks:
64
+
65
+ - unit-tests
66
+
67
+ Result: passed
68
+
69
+ Evidence refs:
70
+
71
+ - unit-tests.md
72
+ `;
73
+ }
74
+ function renderUnitTestsEvidence(changeId) {
75
+ return `# unit-tests evidence for ${changeId}
76
+
77
+ Replace this stub with the real test command, output, and coverage delta. The autonomous resume validator only requires this file to exist and be a safe markdown name listed in checkpoint-1.json validationRefs.
78
+ `;
79
+ }
80
+ function renderResumeInstructions(changeId) {
81
+ return `---
82
+ changeId: ${changeId}
83
+ artifactType: resume-instructions
84
+ status: passed
85
+ ---
86
+
87
+ Resume steps:
88
+
89
+ - Read autonomous-goal-package.json and confirm acceptance criteria.
90
+ - Read autonomous-rd-plan.json and reconcile worker queue with current diff.
91
+ - Read checkpoint-1.json and verify worker queue state matches what is on disk.
92
+ - Read evidence/*.md referenced by checkpoint validationRefs and confirm Result: passed.
93
+
94
+ Preconditions:
95
+
96
+ - Artifact workspace is local and matches changeId ${changeId}.
97
+ - No destructive --apply has run without explicit authorization.
98
+
99
+ Blocked actions:
100
+
101
+ - Resume cannot proceed if checkpoint validationRefs or evidence files are missing or invalid.
102
+
103
+ Next actions:
104
+
105
+ - Run peaks workflow autonomous --change-id ${changeId} --goal "<goal>" --json to recompute the plan.
106
+ - Compare blockedReasons; resolve before reattempting resume.
107
+ `;
108
+ }
109
+ function buildFiles(changeId, goal, createdAt, artifactWorkspacePath) {
110
+ return [
111
+ {
112
+ path: join(artifactWorkspacePath, buildArtifactRelativePath(changeId, 'prd', 'autonomous-goal-package.json')),
113
+ content: renderGoalPackage(changeId, goal)
114
+ },
115
+ {
116
+ path: join(artifactWorkspacePath, buildArtifactRelativePath(changeId, 'rd', 'swarm', 'autonomous-rd-plan.json')),
117
+ content: renderRdPlan(changeId)
118
+ },
119
+ {
120
+ path: join(artifactWorkspacePath, buildArtifactRelativePath(changeId, 'rd', 'swarm', 'checkpoints', 'checkpoint-1.json')),
121
+ content: renderCheckpoint(changeId, createdAt)
122
+ },
123
+ {
124
+ path: join(artifactWorkspacePath, buildArtifactRelativePath(changeId, 'rd', 'swarm', 'evidence', 'unit-tests.md')),
125
+ content: renderUnitTestsEvidence(changeId)
126
+ },
127
+ {
128
+ path: join(artifactWorkspacePath, buildArtifactRelativePath(changeId, 'rd', 'swarm', 'evidence', 'validation-report.md')),
129
+ content: renderValidationReport(changeId)
130
+ },
131
+ {
132
+ path: join(artifactWorkspacePath, buildArtifactRelativePath(changeId, 'rd', 'swarm', 'resume-instructions.md')),
133
+ content: renderResumeInstructions(changeId)
134
+ }
135
+ ];
136
+ }
137
+ export async function writeAutonomousResumeArtifacts(request) {
138
+ validateChangeIdOrThrow(request.changeId);
139
+ const goal = normalizeGoal(request.goal);
140
+ const clock = request.clock ?? defaultClock;
141
+ const createdAt = clock();
142
+ const files = buildFiles(request.changeId, goal, createdAt, request.artifactWorkspacePath);
143
+ if (request.apply !== true) {
144
+ return { applied: false, files };
145
+ }
146
+ for (const file of files) {
147
+ if (await pathExists(file.path)) {
148
+ throw new Error(`Refusing to write: ${file.path} already exists. Remove it before re-running peaks autonomous resume init --apply.`);
149
+ }
150
+ }
151
+ for (const file of files) {
152
+ await mkdir(dirname(file.path), { recursive: true });
153
+ await writeFile(file.path, file.content, 'utf8');
154
+ }
155
+ return { applied: true, files };
156
+ }
@@ -1 +1 @@
1
- export declare const CLI_VERSION = "1.0.14";
1
+ export declare const CLI_VERSION = "1.0.15";
@@ -1 +1 @@
1
- export const CLI_VERSION = "1.0.14";
1
+ export const CLI_VERSION = "1.0.15";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-cli",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "description": "Peaks CLI and short skill family for Claude Code automation.",
5
5
  "author": "SquabbyZ",
6
6
  "license": "MIT",
@@ -13,8 +13,8 @@
13
13
  "properties": {
14
14
  "id": {
15
15
  "type": "string",
16
- "pattern": "^(skill|skill-name|skill-parse|skill-runbook|skill-apply-note|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), 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)."
16
+ "pattern": "^(skill|skill-name|skill-parse|skill-runbook|skill-apply-note|skill-presence|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), 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 }
@@ -7,6 +7,16 @@ description: Product and requirement skill for Peaks. Use when a workflow needs
7
7
 
8
8
  Peaks PRD turns user intent into verifiable product artifacts.
9
9
 
10
+ ## Skill presence (MANDATORY first action)
11
+
12
+ Before any analysis or tool call, immediately run:
13
+
14
+ ```bash
15
+ peaks skill presence:set peaks-prd --mode <mode> --gate startup
16
+ ```
17
+
18
+ Then display: `Peaks Skill: peaks-prd | Gate: startup | Next: <one short action>`. Update with `peaks skill presence:set peaks-prd --mode <mode> --gate <gate>` when gates change. When the role's work ends, run `peaks skill presence:clear`.
19
+
10
20
  ## Responsibilities
11
21
 
12
22
  - clarify goals and non-goals;