peaks-cli 1.2.7 → 1.2.9

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 (54) hide show
  1. package/README.md +12 -0
  2. package/dist/src/cli/commands/core-artifact-commands.js +36 -1
  3. package/dist/src/cli/commands/perf-commands.d.ts +3 -0
  4. package/dist/src/cli/commands/perf-commands.js +41 -0
  5. package/dist/src/cli/commands/progress-close-kill.d.ts +51 -0
  6. package/dist/src/cli/commands/progress-close-kill.js +152 -0
  7. package/dist/src/cli/commands/progress-commands.d.ts +3 -0
  8. package/dist/src/cli/commands/progress-commands.js +348 -0
  9. package/dist/src/cli/commands/progress-start-spawn.d.ts +59 -0
  10. package/dist/src/cli/commands/progress-start-spawn.js +114 -0
  11. package/dist/src/cli/commands/progress-watch-render.d.ts +80 -0
  12. package/dist/src/cli/commands/progress-watch-render.js +308 -0
  13. package/dist/src/cli/commands/project-commands.js +1 -1
  14. package/dist/src/cli/commands/scan-commands.js +22 -0
  15. package/dist/src/cli/program.js +4 -0
  16. package/dist/src/services/config/config-types.d.ts +20 -0
  17. package/dist/src/services/config/config-types.js +5 -1
  18. package/dist/src/services/memory/project-memory-service.d.ts +1 -1
  19. package/dist/src/services/memory/project-memory-service.js +52 -23
  20. package/dist/src/services/perf/perf-baseline-service.d.ts +70 -0
  21. package/dist/src/services/perf/perf-baseline-service.js +213 -0
  22. package/dist/src/services/progress/progress-service.d.ts +179 -0
  23. package/dist/src/services/progress/progress-service.js +276 -0
  24. package/dist/src/services/scan/libraries-service.d.ts +24 -0
  25. package/dist/src/services/scan/libraries-service.js +419 -0
  26. package/dist/src/services/scan/libraries-types.d.ts +59 -0
  27. package/dist/src/services/scan/libraries-types.js +9 -0
  28. package/dist/src/services/session/index.d.ts +1 -1
  29. package/dist/src/services/session/index.js +1 -1
  30. package/dist/src/services/session/session-manager.d.ts +53 -8
  31. package/dist/src/services/session/session-manager.js +150 -3
  32. package/dist/src/services/skills/skill-presence-service.d.ts +27 -1
  33. package/dist/src/services/skills/skill-presence-service.js +112 -9
  34. package/dist/src/services/skills/skill-runbook-service.js +34 -1
  35. package/dist/src/services/workflow/autonomous-resume-writer.js +7 -7
  36. package/dist/src/shared/change-id.d.ts +30 -0
  37. package/dist/src/shared/change-id.js +40 -6
  38. package/dist/src/shared/paths.d.ts +1 -1
  39. package/dist/src/shared/paths.js +2 -1
  40. package/dist/src/shared/version.d.ts +1 -1
  41. package/dist/src/shared/version.js +1 -1
  42. package/package.json +6 -2
  43. package/schemas/library-breaking-changes.data.json +141 -0
  44. package/schemas/library-breaking-changes.meta.json +6 -0
  45. package/schemas/library-breaking-changes.schema.json +50 -0
  46. package/skills/peaks-qa/SKILL.md +25 -0
  47. package/skills/peaks-rd/SKILL.md +221 -2
  48. package/skills/peaks-solo/SKILL.md +76 -316
  49. package/skills/peaks-solo/references/runbook.md +166 -0
  50. package/skills/peaks-solo/references/workflow-gates-and-types.md +177 -0
  51. package/skills/peaks-solo-resume/SKILL.md +81 -0
  52. package/skills/peaks-solo-status/SKILL.md +120 -0
  53. package/skills/peaks-solo-test/SKILL.md +84 -0
  54. package/skills/peaks-txt/SKILL.md +8 -5
@@ -0,0 +1,419 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { join, sep } from 'node:path';
3
+ import { isDirectory, pathExists, readText } from '../../shared/fs.js';
4
+ const SCOPES = [
5
+ 'dependencies',
6
+ 'devDependencies',
7
+ 'peerDependencies',
8
+ 'optionalDependencies'
9
+ ];
10
+ const SCOPE_ORDER = {
11
+ dependencies: 0,
12
+ devDependencies: 1,
13
+ peerDependencies: 2,
14
+ optionalDependencies: 3
15
+ };
16
+ /**
17
+ * Parse the major version from a semver-ish spec.
18
+ *
19
+ * Handles the common shapes:
20
+ * "^5.18.0" → 5
21
+ * "~1.2.3" → 1
22
+ * "1.2.3" → 1
23
+ * ">=1.0.0" → 1
24
+ * "5" → 5
25
+ * "5.x" → 5
26
+ *
27
+ * Returns null for non-semver specs that the LLM should not assume a
28
+ * major for:
29
+ * "workspace:*" → null
30
+ * "file:../..." → null
31
+ * "git+https..." → null
32
+ * "npm:@scope/x@1" → 1 (alias spec, we extract what we can)
33
+ */
34
+ export function parseMajorVersion(spec) {
35
+ // Strip npm alias prefix "npm:" and take whatever comes after the LAST "@"
36
+ // (since alias specs are "npm:<name>@<version>")
37
+ let versionPart = spec;
38
+ if (versionPart.startsWith('npm:')) {
39
+ const atIdx = versionPart.lastIndexOf('@');
40
+ versionPart = atIdx >= 0 ? versionPart.slice(atIdx + 1) : versionPart.slice(4);
41
+ }
42
+ // Strip semver range operators (caret, tilde, comparison ops, exact)
43
+ versionPart = versionPart.replace(/^[\^~>=<]+\s*/, '').trim();
44
+ // Take the first numeric component
45
+ const match = /^(\d+)/.exec(versionPart);
46
+ if (!match)
47
+ return null;
48
+ const n = Number(match[1]);
49
+ return Number.isSafeInteger(n) ? n : null;
50
+ }
51
+ async function readPackageJson(packageJsonPath) {
52
+ if (!(await pathExists(packageJsonPath))) {
53
+ return { exists: false, record: null };
54
+ }
55
+ try {
56
+ const raw = await readText(packageJsonPath);
57
+ const record = JSON.parse(raw);
58
+ return { exists: true, record };
59
+ }
60
+ catch (error) {
61
+ return { exists: true, record: null, error: error.message };
62
+ }
63
+ }
64
+ /**
65
+ * Hand-rolled YAML parser for `pnpm-workspace.yaml`.
66
+ *
67
+ * Only consumes the top-level `packages:` list. Ignores all other top-level
68
+ * keys (`allowBuilds:`, `catalog:`, etc.) and indented blocks. Returns
69
+ * `null` when the file is present but does not contain a `packages:`
70
+ * list — callers should fall through to the next detection source.
71
+ *
72
+ * Supports the common shapes:
73
+ * packages:
74
+ * - 'packages/*'
75
+ * - "apps/web"
76
+ * - tools
77
+ * - packages/hermes-agent/*
78
+ */
79
+ function parsePnpmWorkspaceYaml(content) {
80
+ const lines = content.split(/\r?\n/);
81
+ let inPackagesList = false;
82
+ let packagesIndent = -1;
83
+ const result = [];
84
+ for (const rawLine of lines) {
85
+ // Strip comments (anything after # not inside quotes — close enough for our use)
86
+ const commentIdx = rawLine.indexOf('#');
87
+ const line = (commentIdx >= 0 ? rawLine.slice(0, commentIdx) : rawLine).replace(/\s+$/, '');
88
+ if (line.length === 0)
89
+ continue;
90
+ if (!inPackagesList) {
91
+ // Looking for the top-level `packages:` key.
92
+ const match = /^packages:\s*$/.exec(line);
93
+ if (match) {
94
+ inPackagesList = true;
95
+ }
96
+ continue;
97
+ }
98
+ // Inside the packages list. Items must be more indented than the key.
99
+ const indent = line.match(/^\s*/)?.[0].length ?? 0;
100
+ if (packagesIndent < 0) {
101
+ // First non-empty line after the key — this sets the indent.
102
+ if (indent === 0) {
103
+ // `packages:` was the last meaningful line; an unindented line
104
+ // means the list is empty or the format is unsupported.
105
+ break;
106
+ }
107
+ packagesIndent = indent;
108
+ }
109
+ else if (indent < packagesIndent) {
110
+ // Outdented — the list ended.
111
+ break;
112
+ }
113
+ else if (indent > packagesIndent) {
114
+ // Nested list (e.g. `packages: [{ ... }]`) — unsupported; stop.
115
+ break;
116
+ }
117
+ // Expect `- <value>` or `- '<value>'` or `- "<value>"`.
118
+ const itemMatch = /^\s*-\s+(['"]?)(.+?)\1\s*$/.exec(line);
119
+ if (itemMatch && itemMatch[2] !== undefined) {
120
+ result.push(itemMatch[2]);
121
+ }
122
+ else {
123
+ // Not a list item; the block ended.
124
+ break;
125
+ }
126
+ }
127
+ return result;
128
+ }
129
+ function extractNpmWorkspaces(value) {
130
+ if (Array.isArray(value))
131
+ return value;
132
+ if (value && typeof value === 'object' && Array.isArray(value.packages)) {
133
+ return value.packages;
134
+ }
135
+ return null;
136
+ }
137
+ /**
138
+ * Discover workspace `package.json` paths from the supported monorepo
139
+ * manifest sources. Returns the list of absolute paths plus which
140
+ * detection source won. When no source is present, returns an empty
141
+ * list with `source: null` (caller should fall through to single-package
142
+ * behavior).
143
+ */
144
+ async function discoverWorkspacePackageJsons(projectRoot, warnings) {
145
+ // 1. pnpm-workspace.yaml
146
+ const pnpmWsPath = join(projectRoot, 'pnpm-workspace.yaml');
147
+ if (await pathExists(pnpmWsPath)) {
148
+ try {
149
+ const yaml = await readText(pnpmWsPath);
150
+ const globs = parsePnpmWorkspaceYaml(yaml);
151
+ if (globs && globs.length > 0) {
152
+ const paths = await expandWorkspaceGlobs(projectRoot, globs, warnings);
153
+ return { paths, source: 'pnpm-workspace' };
154
+ }
155
+ }
156
+ catch (error) {
157
+ warnings.push(`pnpm-workspace.yaml present but unreadable: ${error.message}`);
158
+ }
159
+ }
160
+ // 2. package.json `workspaces` field (npm or yarn classic).
161
+ const rootPkgPath = join(projectRoot, 'package.json');
162
+ if (await pathExists(rootPkgPath)) {
163
+ const { record } = await readPackageJson(rootPkgPath);
164
+ if (record) {
165
+ const globs = extractNpmWorkspaces(record.workspaces);
166
+ if (globs && globs.length > 0) {
167
+ const paths = await expandWorkspaceGlobs(projectRoot, globs, warnings);
168
+ return { paths, source: 'package-json-workspaces' };
169
+ }
170
+ }
171
+ }
172
+ // 3. lerna.json `packages` field.
173
+ const lernaPath = join(projectRoot, 'lerna.json');
174
+ if (await pathExists(lernaPath)) {
175
+ try {
176
+ const lernaRaw = await readText(lernaPath);
177
+ const lerna = JSON.parse(lernaRaw);
178
+ if (Array.isArray(lerna.packages) && lerna.packages.every((p) => typeof p === 'string')) {
179
+ const paths = await expandWorkspaceGlobs(projectRoot, lerna.packages, warnings);
180
+ return { paths, source: 'lerna' };
181
+ }
182
+ }
183
+ catch (error) {
184
+ warnings.push(`lerna.json present but unreadable: ${error.message}`);
185
+ }
186
+ }
187
+ return { paths: [], source: null };
188
+ }
189
+ /**
190
+ * Resolve a list of workspace globs against the project tree.
191
+ *
192
+ * Supports shapes with up to two segments of `*` wildcards:
193
+ * - `packages/*`
194
+ * - `packages/hermes-agent/*`
195
+ * - `apps/web` (literal, no wildcard)
196
+ *
197
+ * Deeper patterns (more than one `*` segment) are silently skipped and a
198
+ * warning is emitted. This is by design (no new dependency, narrow scope).
199
+ */
200
+ async function expandWorkspaceGlobs(projectRoot, globs, warnings) {
201
+ const found = new Set();
202
+ for (const glob of globs) {
203
+ const segments = glob.split('/').filter((s) => s.length > 0);
204
+ const wildcardCount = segments.filter((s) => s === '*').length;
205
+ if (wildcardCount > 1) {
206
+ warnings.push(`workspace glob "${glob}" has more than one wildcard; skipped (unsupported).`);
207
+ continue;
208
+ }
209
+ if (segments.length === 0)
210
+ continue;
211
+ if (wildcardCount === 0) {
212
+ // Literal path — treat the last segment as a directory containing package.json.
213
+ const literalDir = join(projectRoot, ...segments);
214
+ if (await isDirectory(literalDir)) {
215
+ const candidate = join(literalDir, 'package.json');
216
+ if (await pathExists(candidate)) {
217
+ found.add(candidate);
218
+ }
219
+ }
220
+ continue;
221
+ }
222
+ // Exactly one wildcard — it must be the LAST segment.
223
+ const wildcardIdx = segments.indexOf('*');
224
+ if (wildcardIdx !== segments.length - 1) {
225
+ warnings.push(`workspace glob "${glob}" has non-trailing wildcard; skipped.`);
226
+ continue;
227
+ }
228
+ const parentSegments = segments.slice(0, wildcardIdx);
229
+ const parentDir = join(projectRoot, ...parentSegments);
230
+ if (!(await isDirectory(parentDir)))
231
+ continue;
232
+ let entries;
233
+ try {
234
+ entries = await readdir(parentDir, { withFileTypes: true });
235
+ }
236
+ catch (error) {
237
+ warnings.push(`workspace glob "${glob}": could not read ${parentDir}: ${error.message}`);
238
+ continue;
239
+ }
240
+ for (const entry of entries) {
241
+ if (!entry.isDirectory())
242
+ continue;
243
+ const childPkg = join(parentDir, entry.name, 'package.json');
244
+ if (await pathExists(childPkg)) {
245
+ found.add(childPkg);
246
+ }
247
+ }
248
+ }
249
+ return Array.from(found).sort();
250
+ }
251
+ /**
252
+ * Parse a single `package.json` record into `LibraryEntry` rows.
253
+ * Returns the entries and the per-scope tallies for that one package.
254
+ */
255
+ function extractEntriesFromPackageJson(record) {
256
+ const byScope = {
257
+ dependencies: 0,
258
+ devDependencies: 0,
259
+ peerDependencies: 0,
260
+ optionalDependencies: 0
261
+ };
262
+ const entries = [];
263
+ for (const scope of SCOPES) {
264
+ const deps = record[scope];
265
+ if (!deps)
266
+ continue;
267
+ for (const [name, version] of Object.entries(deps)) {
268
+ entries.push({
269
+ name,
270
+ version,
271
+ major: parseMajorVersion(version),
272
+ scope,
273
+ ecosystem: 'npm'
274
+ });
275
+ byScope[scope] += 1;
276
+ }
277
+ }
278
+ return { entries, byScope };
279
+ }
280
+ export async function scanLibraries(options) {
281
+ const { projectRoot } = options;
282
+ const warnings = [];
283
+ const libraries = [];
284
+ const byScope = {
285
+ dependencies: 0,
286
+ devDependencies: 0,
287
+ peerDependencies: 0,
288
+ optionalDependencies: 0
289
+ };
290
+ const workspaces = [];
291
+ // 1. Discover monorepo workspaces (if any). Discovery is independent of
292
+ // whether the root `package.json` exists; we still attempt it.
293
+ const { paths: workspacePkgPaths, source } = await discoverWorkspacePackageJsons(projectRoot, warnings);
294
+ const isMonorepo = source !== null && workspacePkgPaths.length > 0;
295
+ if (isMonorepo) {
296
+ // Monorepo mode: scan the root + every discovered workspace package.json.
297
+ const rootPkgPath = join(projectRoot, 'package.json');
298
+ const allPaths = [];
299
+ if (await pathExists(rootPkgPath)) {
300
+ allPaths.push(rootPkgPath);
301
+ }
302
+ // Dedupe: the root might also be matched by a glob (rare, but possible).
303
+ for (const p of workspacePkgPaths) {
304
+ if (!allPaths.includes(p))
305
+ allPaths.push(p);
306
+ }
307
+ // Recursive descent: for each discovered workspace dir, also pick up
308
+ // any nested `package.json` one level deeper. This matches the pnpm
309
+ // convention where a workspace package (e.g. `hermes-agent`) can be
310
+ // a container for sub-workspaces (e.g. `hermes-agent/ui-tui`). The
311
+ // outer pnpm-workspace.yaml often only declares the top-level glob,
312
+ // so a literal-glob-only scan would miss the nested packages.
313
+ const expanded = new Set(allPaths);
314
+ for (const pkgPath of allPaths) {
315
+ const dir = pkgPath.replace(/[\\/]package\.json$/, '');
316
+ if (!(await isDirectory(dir)))
317
+ continue;
318
+ let entries;
319
+ try {
320
+ entries = await readdir(dir, { withFileTypes: true });
321
+ }
322
+ catch {
323
+ continue;
324
+ }
325
+ for (const entry of entries) {
326
+ if (!entry.isDirectory())
327
+ continue;
328
+ const nestedPkg = join(dir, entry.name, 'package.json');
329
+ if (await pathExists(nestedPkg)) {
330
+ expanded.add(nestedPkg);
331
+ }
332
+ }
333
+ }
334
+ allPaths.length = 0;
335
+ allPaths.push(...Array.from(expanded).sort());
336
+ for (const pkgPath of allPaths) {
337
+ const { exists, record, error } = await readPackageJson(pkgPath);
338
+ if (!exists)
339
+ continue;
340
+ if (record === null) {
341
+ warnings.push(`${normalizePathForDisplay(pkgPath)} is not valid JSON: ${error ?? 'unknown parse error'}`);
342
+ continue;
343
+ }
344
+ const { entries, byScope: pkgByScope } = extractEntriesFromPackageJson(record);
345
+ libraries.push(...entries);
346
+ for (const scope of SCOPES) {
347
+ byScope[scope] += pkgByScope[scope];
348
+ }
349
+ // For per-workspace `workspaces[]`, only list non-root paths; the
350
+ // root is implicit and lives at `projectRoot`.
351
+ const isRoot = pkgPath === rootPkgPath;
352
+ if (!isRoot) {
353
+ const entry = {
354
+ path: pkgPath,
355
+ count: entries.length
356
+ };
357
+ if (record.name !== undefined)
358
+ entry.name = record.name;
359
+ if (record.version !== undefined)
360
+ entry.version = record.version;
361
+ workspaces.push(entry);
362
+ }
363
+ }
364
+ }
365
+ else {
366
+ // Single-package mode (today's behavior). Preserved byte-for-byte
367
+ // apart from the new additive `workspaces: []` field.
368
+ const { exists, record, error } = await readPackageJson(join(projectRoot, 'package.json'));
369
+ if (!exists) {
370
+ warnings.push('package.json not found; nothing to scan.');
371
+ return {
372
+ projectRoot,
373
+ libraries,
374
+ totalCount: 0,
375
+ byScope,
376
+ workspaces: [],
377
+ scannedAt: new Date().toISOString(),
378
+ warnings
379
+ };
380
+ }
381
+ if (record === null) {
382
+ warnings.push(`package.json is not valid JSON: ${error ?? 'unknown parse error'}`);
383
+ return {
384
+ projectRoot,
385
+ libraries,
386
+ totalCount: 0,
387
+ byScope,
388
+ workspaces: [],
389
+ scannedAt: new Date().toISOString(),
390
+ warnings
391
+ };
392
+ }
393
+ const { entries, byScope: pkgByScope } = extractEntriesFromPackageJson(record);
394
+ libraries.push(...entries);
395
+ for (const scope of SCOPES) {
396
+ byScope[scope] += pkgByScope[scope];
397
+ }
398
+ }
399
+ // Sort: by name (alphabetical), then by scope order (deps first, then dev, peer, optional)
400
+ libraries.sort((a, b) => {
401
+ if (a.name !== b.name)
402
+ return a.name.localeCompare(b.name);
403
+ return SCOPE_ORDER[a.scope] - SCOPE_ORDER[b.scope];
404
+ });
405
+ return {
406
+ projectRoot,
407
+ libraries,
408
+ totalCount: libraries.length,
409
+ byScope,
410
+ workspaces,
411
+ scannedAt: new Date().toISOString(),
412
+ warnings
413
+ };
414
+ }
415
+ function normalizePathForDisplay(p) {
416
+ // Surface posix-style separators in warnings for consistency with the
417
+ // existing 'package.json is not valid JSON' message shape.
418
+ return p.split(sep).join('/');
419
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Library version scan types.
3
+ * Joins the existing scan family (archetype, existing-system, type-sanity,
4
+ * acceptance-coverage, diff-vs-scope, file-size) but is intentionally scoped
5
+ * to library version enumeration only. Does NOT extend scan-types.ts —
6
+ * each scan service ships its own types to keep the scan-types module
7
+ * focused on the archetype + existing-system pair (see src/services/scan/scan-types.ts).
8
+ */
9
+ export type Ecosystem = 'npm';
10
+ export type DependencyScope = 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies';
11
+ export type LibraryEntry = {
12
+ /** npm package name (e.g. "antd", "@mui/material", "react-router-dom"). */
13
+ name: string;
14
+ /** Raw version spec as written in package.json (e.g. "^5.18.0", "workspace:*", "git+https://..."). */
15
+ version: string;
16
+ /**
17
+ * Parsed major version, or null when the spec is non-semver (e.g.
18
+ * "workspace:*", "file:../local", "git+https://..."). The LLM should
19
+ * treat null as "cannot determine major; consult breaking-changes table
20
+ * by other signals (e.g. lockfile or import statement shape)".
21
+ */
22
+ major: number | null;
23
+ scope: DependencyScope;
24
+ ecosystem: Ecosystem;
25
+ };
26
+ /**
27
+ * Per-workspace provenance for monorepo scans.
28
+ *
29
+ * `path` is the absolute path of the `package.json` that contributed
30
+ * libraries. `count` is the number of `LibraryEntry` rows produced by
31
+ * reading that single `package.json` (i.e. NOT the aggregate across
32
+ * the whole monorepo — use `LibraryReport.totalCount` for the aggregate).
33
+ *
34
+ * `name` and `version` are the workspace's own `name` / `version` from
35
+ * its `package.json`, when present. They are optional because some
36
+ * workspace `package.json` files omit them.
37
+ */
38
+ export type WorkspaceEntry = {
39
+ path: string;
40
+ count: number;
41
+ name?: string;
42
+ version?: string;
43
+ };
44
+ export type LibraryReport = {
45
+ projectRoot: string;
46
+ libraries: LibraryEntry[];
47
+ totalCount: number;
48
+ byScope: Record<DependencyScope, number>;
49
+ /**
50
+ * Per-workspace provenance for monorepo (pnpm / npm / yarn workspaces,
51
+ * lerna) projects. Empty for single-package projects so the field is
52
+ * always present (additive; consumers can rely on the shape).
53
+ */
54
+ workspaces: WorkspaceEntry[];
55
+ /** ISO timestamp at scan time. */
56
+ scannedAt: string;
57
+ /** Soft signals — e.g. "package.json not found" or "package.json is not valid JSON". */
58
+ warnings: string[];
59
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Library version scan types.
3
+ * Joins the existing scan family (archetype, existing-system, type-sanity,
4
+ * acceptance-coverage, diff-vs-scope, file-size) but is intentionally scoped
5
+ * to library version enumeration only. Does NOT extend scan-types.ts —
6
+ * each scan service ships its own types to keep the scan-types module
7
+ * focused on the archetype + existing-system pair (see src/services/scan/scan-types.ts).
8
+ */
9
+ export {};
@@ -1 +1 @@
1
- export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, setCurrentSessionBinding, type SessionInfo, type SessionMeta } from './session-manager.js';
1
+ export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, setCurrentSessionBinding, rotateSessionBinding, type SessionInfo, type SessionMeta } from './session-manager.js';
@@ -1 +1 @@
1
- export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, setCurrentSessionBinding } from './session-manager.js';
1
+ export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, setCurrentSessionBinding, rotateSessionBinding } from './session-manager.js';
@@ -19,7 +19,32 @@ export type SessionMeta = {
19
19
  createdAt: string;
20
20
  lastActivity?: string;
21
21
  projectRoot: string;
22
+ /**
23
+ * The outer (harness / IDE / plugin) session id that
24
+ * `ensureSession` was called from. Sourced from
25
+ * `PEAKS_OUTER_SESSION_ID` env var, with `CLAUDE_CODE_SESSION_ID`
26
+ * as a Claude-Code fallback. Stamped once at session creation;
27
+ * later presence writes can compare against this to detect an
28
+ * outer-session swap and AskUserQuestion the user about rolling
29
+ * a new peaks session. Sessions predating the field simply
30
+ * have it undefined; presence-mismatch detection skips those
31
+ * (no false positives on legacy data).
32
+ */
33
+ outerSessionId?: string;
22
34
  };
35
+ /**
36
+ * Drop the project-level session binding (`.peaks/.session.json`)
37
+ * so the next `ensureSession()` call auto-generates a fresh
38
+ * session id. The on-disk session directory is left intact —
39
+ * rotating does NOT delete the user's data, it just unbinds the
40
+ * project from that session.
41
+ *
42
+ * Returns the id of the session that was unbound, or `null` if
43
+ * no binding was present. The caller is expected to do something
44
+ * with that — at minimum surface it in the CLI response so the
45
+ * user can find the directory again if they need to.
46
+ */
47
+ export declare function rotateSessionBinding(projectRoot: string): string | null;
23
48
  /**
24
49
  * Bind the project's current session to the given session id by writing
25
50
  * `.peaks/.session.json`. The single-session binding is the source of truth
@@ -50,14 +75,6 @@ export declare function setSessionTitle(projectRoot: string, sessionId: string,
50
75
  * Returns sessions sorted by sessionId descending (most recent first).
51
76
  */
52
77
  export declare function listSessionMetas(projectRoot: string): SessionMeta[];
53
- /**
54
- * Get or create the current session for a project.
55
- * If a valid session already exists, returns it.
56
- * Otherwise, creates a new session with auto-generated ID.
57
- *
58
- * @param projectRoot - Root directory of the project
59
- * @returns Session ID (e.g., "2026-05-26-session-a3f8b1")
60
- */
61
78
  export declare function ensureSession(projectRoot: string): Promise<string>;
62
79
  /**
63
80
  * Get the current session ID without creating a new one.
@@ -67,6 +84,34 @@ export declare function ensureSession(projectRoot: string): Promise<string>;
67
84
  * @returns Session ID or null
68
85
  */
69
86
  export declare function getSessionId(projectRoot: string): string | null;
87
+ /**
88
+ * Resolve the current session id with canonicalize-on-read
89
+ * semantics. This is the variant the progress subcommands
90
+ * (step / watch / start / close) use, because the legacy
91
+ * `getSessionId` returns null any time the stored
92
+ * `projectRoot` form differs from the caller-passed form
93
+ * (e.g. stored is "." from inside the project dir; caller
94
+ * is the absolute realpath). When `getSessionId` returns
95
+ * null, callers like `ensureSession` create a brand-new
96
+ * session and overwrite the binding — which is what the
97
+ * user observed as the "mid-dogfood rebind" bug.
98
+ *
99
+ * The fix is to canonicalize both sides of the compare
100
+ * (realpath, then optionally resolve relative stored
101
+ * against the caller's project root). The two forms of
102
+ * the same physical directory now compare equal, and the
103
+ * existing binding is found instead of being overwritten.
104
+ *
105
+ * Use this instead of `getSessionId` only when the
106
+ * caller is operating on a user-supplied `--project` flag
107
+ * and the binding may have been written by a CLI invocation
108
+ * that was running from inside the project dir (the common
109
+ * peaks-solo / peaks-sop scenario). Other modules depend
110
+ * on the strict-equality semantics of `getSessionId` (the
111
+ * "no binding" fallback path is part of their contract),
112
+ * so this variant is opt-in.
113
+ */
114
+ export declare function getSessionIdCanonical(projectRoot: string): string | null;
70
115
  /**
71
116
  * Get the absolute path to the current session directory.
72
117
  * Creates the session if it doesn't exist.