peaks-cli 1.3.0 → 1.3.2
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/README.md +62 -46
- package/bin/peaks.js +0 -0
- package/dist/src/cli/commands/core-artifact-commands.js +49 -11
- package/dist/src/cli/commands/hooks-commands.js +24 -9
- package/dist/src/cli/commands/progress-commands.js +26 -2
- package/dist/src/cli/commands/request-commands.js +5 -0
- package/dist/src/cli/commands/slice-commands.d.ts +3 -0
- package/dist/src/cli/commands/slice-commands.js +44 -0
- package/dist/src/cli/commands/workflow-commands.js +3 -3
- package/dist/src/cli/commands/workspace-commands.d.ts +63 -0
- package/dist/src/cli/commands/workspace-commands.js +349 -12
- package/dist/src/cli/program.js +4 -0
- package/dist/src/services/artifacts/artifact-prerequisites.d.ts +29 -1
- package/dist/src/services/artifacts/artifact-prerequisites.js +69 -5
- package/dist/src/services/artifacts/request-artifact-service.d.ts +22 -0
- package/dist/src/services/artifacts/request-artifact-service.js +214 -56
- package/dist/src/services/doctor/doctor-service.d.ts +69 -0
- package/dist/src/services/doctor/doctor-service.js +296 -3
- package/dist/src/services/progress/progress-service.d.ts +26 -0
- package/dist/src/services/progress/progress-service.js +25 -0
- package/dist/src/services/sc/sc-service.js +71 -13
- package/dist/src/services/scan/acceptance-coverage-service.js +6 -2
- package/dist/src/services/session/session-manager.d.ts +22 -1
- package/dist/src/services/session/session-manager.js +149 -30
- package/dist/src/services/skills/hooks-settings-service.d.ts +25 -3
- package/dist/src/services/skills/hooks-settings-service.js +57 -13
- package/dist/src/services/slice/slice-check-service.d.ts +2 -0
- package/dist/src/services/slice/slice-check-service.js +267 -0
- package/dist/src/services/slice/slice-check-types.d.ts +70 -0
- package/dist/src/services/slice/slice-check-types.js +18 -0
- package/dist/src/services/workflow/pipeline-verify-service.d.ts +5 -2
- package/dist/src/services/workflow/pipeline-verify-service.js +35 -35
- package/dist/src/services/workspace/migrate-service.d.ts +2 -0
- package/dist/src/services/workspace/migrate-service.js +606 -0
- package/dist/src/services/workspace/migrate-types.d.ts +127 -0
- package/dist/src/services/workspace/migrate-types.js +21 -0
- package/dist/src/services/workspace/reconcile-service.d.ts +33 -0
- package/dist/src/services/workspace/reconcile-service.js +160 -42
- package/dist/src/services/workspace/reconcile-types.d.ts +25 -0
- package/dist/src/services/workspace/workspace-service.d.ts +11 -0
- package/dist/src/services/workspace/workspace-service.js +71 -24
- package/dist/src/shared/change-id.d.ts +59 -0
- package/dist/src/shared/change-id.js +194 -16
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +10 -2
- package/schemas/doctor-report.schema.json +2 -2
- package/skills/peaks-qa/SKILL.md +1 -0
- package/skills/peaks-rd/SKILL.md +2 -1
- package/skills/peaks-solo/SKILL.md +17 -1
- package/skills/peaks-solo/references/micro-cycle.md +155 -0
- package/skills/peaks-txt/SKILL.md +2 -0
- package/skills/peaks-ui/SKILL.md +1 -0
|
@@ -1,19 +1,110 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
1
4
|
import { initWorkspace, InvalidSessionIdError, ConflictingSessionError } from '../../services/workspace/workspace-service.js';
|
|
2
5
|
import { reconcileWorkspace } from '../../services/workspace/reconcile-service.js';
|
|
6
|
+
import { migrateWorkspace } from '../../services/workspace/migrate-service.js';
|
|
3
7
|
import { ensureSession } from '../../services/session/session-manager.js';
|
|
4
8
|
import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
|
|
9
|
+
import { applyHookInstall, readHookStatus } from '../../services/skills/hooks-settings-service.js';
|
|
5
10
|
import { fail, ok } from '../../shared/result.js';
|
|
6
11
|
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
12
|
+
/** Sticky decision marker for the first-time "install hooks" prompt. */
|
|
13
|
+
const HOOKS_DECISION_REL_PATH = '.peaks/.peaks-init-hooks-decision.json';
|
|
14
|
+
function readDecisionMarker(projectRoot) {
|
|
15
|
+
const path = join(projectRoot, HOOKS_DECISION_REL_PATH);
|
|
16
|
+
if (!existsSync(path))
|
|
17
|
+
return null;
|
|
18
|
+
try {
|
|
19
|
+
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
20
|
+
if (data.version !== 1)
|
|
21
|
+
return null;
|
|
22
|
+
if (data.decision !== 'installed' && data.decision !== 'skipped')
|
|
23
|
+
return null;
|
|
24
|
+
if (typeof data.decidedAt !== 'string')
|
|
25
|
+
return null;
|
|
26
|
+
if (data.scope !== 'project' && data.scope !== 'global')
|
|
27
|
+
return null;
|
|
28
|
+
return {
|
|
29
|
+
version: 1,
|
|
30
|
+
decision: data.decision,
|
|
31
|
+
decidedAt: data.decidedAt,
|
|
32
|
+
scope: data.scope
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function writeDecisionMarker(projectRoot, decision) {
|
|
40
|
+
const path = join(projectRoot, HOOKS_DECISION_REL_PATH);
|
|
41
|
+
const dir = join(projectRoot, '.peaks');
|
|
42
|
+
mkdirSync(dir, { recursive: true });
|
|
43
|
+
const marker = {
|
|
44
|
+
version: 1,
|
|
45
|
+
decision,
|
|
46
|
+
decidedAt: new Date().toISOString(),
|
|
47
|
+
scope: 'project'
|
|
48
|
+
};
|
|
49
|
+
writeFileSync(path, JSON.stringify(marker, null, 2) + '\n', 'utf8');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Read a yes/no answer from stdin. Returns `true` for empty / Y / y,
|
|
53
|
+
* `false` for N / n, or `null` when stdin is not a TTY (the caller falls
|
|
54
|
+
* back to the no-prompt path). Times out after 30s so a piped-but-blocked
|
|
55
|
+
* stdin never hangs the CLI.
|
|
56
|
+
*/
|
|
57
|
+
function promptYesNo(question) {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
if (process.stdin.isTTY !== true) {
|
|
60
|
+
resolve(null);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
|
|
64
|
+
const timer = setTimeout(() => {
|
|
65
|
+
rl.close();
|
|
66
|
+
resolve(null);
|
|
67
|
+
}, 30_000);
|
|
68
|
+
rl.question(question, (answer) => {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
rl.close();
|
|
71
|
+
const trimmed = answer.trim().toLowerCase();
|
|
72
|
+
if (trimmed === '' || trimmed === 'y' || trimmed === 'yes') {
|
|
73
|
+
resolve(true);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (trimmed === 'n' || trimmed === 'no') {
|
|
77
|
+
resolve(false);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Treat anything else as "no" — the user can re-run with --install-hooks
|
|
81
|
+
// if they want a different answer. We never throw from this prompt.
|
|
82
|
+
resolve(false);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
7
86
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
8
87
|
const DEFAULT_RECONCILE_AGE_DAYS = 7;
|
|
9
88
|
export function registerWorkspaceCommands(program, io) {
|
|
10
89
|
const workspace = program.command('workspace').description('Manage the Peaks per-session artifact workspace (.peaks/<session-id>/)');
|
|
11
90
|
addJsonOption(workspace
|
|
12
91
|
.command('init')
|
|
13
|
-
.description('Create the .peaks/<session-id>/ directory
|
|
92
|
+
.description('Create the .peaks/_runtime/<session-id>/ directory with ONLY the session.json metadata file (slice 006: role subdirs prd/ui/rd/qa/sc/txt and the system/ subdir are created lazily by writers, not pre-created at init). When --change-id is given, also creates the .peaks/<change-id>/ dir. Pass --session-id to use a specific id, or omit it to auto-generate one (and adopt an existing binding if present). On the first call for a project, also handles the one-time "install peaks hooks" decision (sticky-marker stored in .peaks/.peaks-init-hooks-decision.json).')
|
|
14
93
|
.requiredOption('--project <path>', 'target project root')
|
|
15
94
|
.option('--session-id <id>', 'optional session id in YYYY-MM-DD-<kebab-slug> format. When omitted, the CLI is the single source of truth: an existing binding is reused, otherwise a fresh id is auto-generated.')
|
|
16
|
-
.option('--allow-session-rebind', 'overwrite an existing session binding when the requested session id differs from the project current one', false)
|
|
95
|
+
.option('--allow-session-rebind', 'overwrite an existing session binding when the requested session id differs from the project current one', false)
|
|
96
|
+
.option('--change-id <id>', 'bind the change-id for reviewable artifacts (writes route to .peaks/<change-id>/<role>/, tracked in git). When omitted, the change-id binding is left unchanged.', (value) => {
|
|
97
|
+
if (value.length === 0) {
|
|
98
|
+
throw new Error('--change-id must not be empty');
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
})
|
|
102
|
+
.option('--install-hooks <mode>', 'first-time hooks install behaviour: ask (default in TTY, prompt once + sticky-marker), auto (default in --json / non-TTY, install silently + sticky-marker), skip (sticky-marker skipped, do not install)', (value) => {
|
|
103
|
+
if (value !== 'ask' && value !== 'auto' && value !== 'skip') {
|
|
104
|
+
throw new Error(`--install-hooks must be one of: ask, auto, skip (got "${value}")`);
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
})).action(async (options) => {
|
|
17
108
|
try {
|
|
18
109
|
// Resolve the session id. Two paths:
|
|
19
110
|
// - explicit --session-id: use it as the requested binding target
|
|
@@ -43,7 +134,8 @@ export function registerWorkspaceCommands(program, io) {
|
|
|
43
134
|
const report = await initWorkspace({
|
|
44
135
|
projectRoot,
|
|
45
136
|
sessionId,
|
|
46
|
-
allowSessionRebind: options.allowSessionRebind === true
|
|
137
|
+
allowSessionRebind: options.allowSessionRebind === true,
|
|
138
|
+
...(options.changeId !== undefined ? { changeId: options.changeId } : {})
|
|
47
139
|
});
|
|
48
140
|
const nextActions = [];
|
|
49
141
|
if (report.previousSessionId !== null && report.bound) {
|
|
@@ -55,7 +147,34 @@ export function registerWorkspaceCommands(program, io) {
|
|
|
55
147
|
else {
|
|
56
148
|
nextActions.push('Run `peaks scan archetype --project <path> --json` next to populate rd/project-scan.md.');
|
|
57
149
|
}
|
|
58
|
-
|
|
150
|
+
// First-time hooks install decision. Sticky-marker at
|
|
151
|
+
// .peaks/.peaks-init-hooks-decision.json records the user's answer
|
|
152
|
+
// (or the auto-decision) so subsequent inits for new sessions in the
|
|
153
|
+
// same project do not re-prompt. The marker is the only state that
|
|
154
|
+
// survives across sessions — without it, every new session would
|
|
155
|
+
// re-trigger the question.
|
|
156
|
+
const hooksOutcome = await resolveFirstTimeHooksInstall({
|
|
157
|
+
projectRoot,
|
|
158
|
+
...(options.installHooks !== undefined ? { explicitMode: options.installHooks } : {}),
|
|
159
|
+
jsonMode: options.json === true
|
|
160
|
+
});
|
|
161
|
+
if (hooksOutcome.decision === 'installed') {
|
|
162
|
+
nextActions.push(hooksOutcome.action === 'reinstalled'
|
|
163
|
+
? 'Re-installed the peaks-managed PreToolUse hooks (Bash→gate enforce, Task→progress start) — the marker said installed but the hooks were missing.'
|
|
164
|
+
: 'Installed the peaks-managed PreToolUse hooks (Bash→gate enforce, Task→progress start). Restart Claude Code so the hooks take effect.');
|
|
165
|
+
}
|
|
166
|
+
else if (hooksOutcome.action === 'first-decision' && hooksOutcome.decision === 'skipped') {
|
|
167
|
+
nextActions.push('Skipped peaks-managed hook install for this project. Re-run with --install-hooks=auto (or peaks hooks install) to install later.');
|
|
168
|
+
}
|
|
169
|
+
printResult(io, ok('workspace.init', {
|
|
170
|
+
...report,
|
|
171
|
+
hooksInstall: {
|
|
172
|
+
decision: hooksOutcome.decision,
|
|
173
|
+
action: hooksOutcome.action,
|
|
174
|
+
scope: hooksOutcome.scope,
|
|
175
|
+
...(hooksOutcome.reason !== undefined ? { reason: hooksOutcome.reason } : {})
|
|
176
|
+
}
|
|
177
|
+
}, [], nextActions), options.json);
|
|
59
178
|
}
|
|
60
179
|
catch (error) {
|
|
61
180
|
if (error instanceof InvalidSessionIdError) {
|
|
@@ -80,14 +199,24 @@ export function registerWorkspaceCommands(program, io) {
|
|
|
80
199
|
});
|
|
81
200
|
addJsonOption(workspace
|
|
82
201
|
.command('reconcile')
|
|
83
|
-
.description('Scan .peaks/2026-MM-DD-session-*/ directories and
|
|
84
|
-
'
|
|
85
|
-
'
|
|
86
|
-
'.peaks/.
|
|
87
|
-
'
|
|
88
|
-
|
|
89
|
-
'
|
|
90
|
-
'
|
|
202
|
+
.description('Scan .peaks/2026-MM-DD-session-*/ directories and consolidate the runtime state. ' +
|
|
203
|
+
'By default (no --apply) the command performs four actions:\n' +
|
|
204
|
+
' 1. Migrates legacy runtime files into .peaks/_runtime/: ' +
|
|
205
|
+
'.peaks/.session.json -> .peaks/_runtime/session.json, ' +
|
|
206
|
+
'.peaks/.active-skill.json -> .peaks/_runtime/active-skill.json, ' +
|
|
207
|
+
'.peaks/sop-state/ -> .peaks/_runtime/sop-state/ ' +
|
|
208
|
+
'(idempotent; no-op if already on the new layout).\n' +
|
|
209
|
+
' 2. Re-points .peaks/_runtime/session.json to the canonical session ' +
|
|
210
|
+
'using a 4-tier heuristic: active-skill binding -> latest session.json mtime -> ' +
|
|
211
|
+
'latest any-file mtime -> dir-name sort.\n' +
|
|
212
|
+
' 3. (slice 006) Syncs the single change/<sid>/ live marker under ' +
|
|
213
|
+
'.peaks/_runtime/change/. The marker is an empty directory; every other ' +
|
|
214
|
+
'entry under change/ is removed. Also cleans up the F3-introduced ' +
|
|
215
|
+
'.peaks/_runtime/<sid>/system/ subdir (no-op if already absent).\n' +
|
|
216
|
+
' 4. REPORTS (but does not delete) session dirs older than --older-than <days> ' +
|
|
217
|
+
`(default ${DEFAULT_RECONCILE_AGE_DAYS}) as deletion candidates; this is the only step that is dry-run by default.\n` +
|
|
218
|
+
'Pass --apply to additionally REMOVE the listed candidate dirs (destructive). ' +
|
|
219
|
+
'Migration (1), repoint (2), and marker sync (3) always run regardless of --apply.')
|
|
91
220
|
.requiredOption('--project <path>', 'target project root')
|
|
92
221
|
.option('--apply', 'actually delete the deletion candidates (destructive); without it, dry-run only', false)
|
|
93
222
|
.option('--older-than <days>', `age threshold in days for deletion candidates (default: ${DEFAULT_RECONCILE_AGE_DAYS})`, (value) => Number.parseFloat(value))).action((options) => {
|
|
@@ -123,6 +252,18 @@ export function registerWorkspaceCommands(program, io) {
|
|
|
123
252
|
if (!apply && result.wouldDelete.length > 0) {
|
|
124
253
|
nextActions.push(`Re-run with --apply to delete ${result.wouldDelete.length} candidate dir(s).`);
|
|
125
254
|
}
|
|
255
|
+
if (result.changeMarker.created !== null) {
|
|
256
|
+
nextActions.push(`Synced change/<${result.changeMarker.created}>/ live marker.`);
|
|
257
|
+
}
|
|
258
|
+
else if (result.canonicalSessionId !== null) {
|
|
259
|
+
nextActions.push(`change/<${result.canonicalSessionId}>/ live marker already in place.`);
|
|
260
|
+
}
|
|
261
|
+
if (result.changeMarker.removed.length > 0) {
|
|
262
|
+
nextActions.push(`Removed ${result.changeMarker.removed.length} stale change/<oldSid>/ marker(s).`);
|
|
263
|
+
}
|
|
264
|
+
if (result.systemCleaned.length > 0) {
|
|
265
|
+
nextActions.push(`Removed ${result.systemCleaned.length} F3 system/ subdir(s).`);
|
|
266
|
+
}
|
|
126
267
|
printResult(io, ok('workspace.reconcile', result, warnings, nextActions), options.json);
|
|
127
268
|
if (result.errors.length > 0) {
|
|
128
269
|
process.exitCode = 1;
|
|
@@ -133,4 +274,200 @@ export function registerWorkspaceCommands(program, io) {
|
|
|
133
274
|
process.exitCode = 1;
|
|
134
275
|
}
|
|
135
276
|
});
|
|
277
|
+
addJsonOption(workspace
|
|
278
|
+
.command('migrate')
|
|
279
|
+
.description('Migrate legacy `.peaks/<session-id>/<role>/<file>` content into the new layout: ' +
|
|
280
|
+
'`.peaks/retrospective/<change-id>/<role>/<file>`. Each file is routed by a 4-tier ' +
|
|
281
|
+
'change-id resolver (filename regex → content H1 → body frontmatter → per-session fallback ' +
|
|
282
|
+
'to the most recent rd/requests entry). Cross-cutting files (project-scan, perf-baseline) ' +
|
|
283
|
+
'and transient runtime files (session.json, system/) are skipped with reasons in the ' +
|
|
284
|
+
'response. By default the command is a dry-run: it reports the planned moves + conflicts ' +
|
|
285
|
+
'and the session dirs that WOULD be deleted. Pass --apply to actually `git mv` the files ' +
|
|
286
|
+
'and `rm -rf` the emptied session dirs. Idempotent: re-running on an already-migrated tree ' +
|
|
287
|
+
'is a no-op (all files report conflicts with identical content).' +
|
|
288
|
+
'\n\nSlice 003 (--to-runtime): moves every top-level `.peaks/<sid>/` to `.peaks/_runtime/<sid>/` ' +
|
|
289
|
+
'for projects still on the pre-runtime-layer layout. Idempotent: re-running on a tree ' +
|
|
290
|
+
'that is already canonical is a no-op. F15 carve-out: top-level `rd/project-scan.md` is ' +
|
|
291
|
+
'never overwritten when the runtime copy already exists with different content.')
|
|
292
|
+
.requiredOption('--project <path>', 'target project root')
|
|
293
|
+
.option('--apply', 'actually `git mv` the files and delete the emptied session dirs (destructive); without it, dry-run only', false)
|
|
294
|
+
.option('--to-runtime', 'slice 003: also consolidate every top-level .peaks/<sid>/ dir into .peaks/_runtime/<sid>/. Idempotent; conflicts are logged but never overwrite.', false)).action(async (options) => {
|
|
295
|
+
try {
|
|
296
|
+
const projectRoot = resolveCanonicalProjectRoot(options.project);
|
|
297
|
+
const apply = options.apply === true;
|
|
298
|
+
const toRuntime = options.toRuntime === true;
|
|
299
|
+
const result = await migrateWorkspace({ projectRoot, apply, toRuntime });
|
|
300
|
+
const warnings = [];
|
|
301
|
+
if (result.sessions.length === 0 && (result.toRuntimePlans?.length ?? 0) === 0) {
|
|
302
|
+
warnings.push('No legacy session directories found under .peaks/. Nothing to migrate.');
|
|
303
|
+
}
|
|
304
|
+
else if (result.wouldMove.length === 0 && (result.toRuntimePlans?.length ?? 0) === 0) {
|
|
305
|
+
warnings.push('Legacy session dirs found but no reviewable content to migrate (all files were cross-cutting or transient).');
|
|
306
|
+
}
|
|
307
|
+
const nextActions = [];
|
|
308
|
+
if (!apply && result.wouldMove.length > 0) {
|
|
309
|
+
nextActions.push(`Re-run with --apply to perform ${result.wouldMove.length} move(s) and delete ${result.wouldDeleteSessions.length} session dir(s).`);
|
|
310
|
+
}
|
|
311
|
+
if (result.conflicts.length > 0) {
|
|
312
|
+
nextActions.push(`${result.conflicts.length} file(s) already exist at the target path; review before --apply (or re-run after a partial migrate).`);
|
|
313
|
+
}
|
|
314
|
+
if (toRuntime) {
|
|
315
|
+
const plans = result.toRuntimePlans ?? [];
|
|
316
|
+
if (apply) {
|
|
317
|
+
if ((result.toRuntimeMoved?.length ?? 0) > 0) {
|
|
318
|
+
nextActions.push(`Moved ${result.toRuntimeMoved?.length} top-level session dir(s) to .peaks/_runtime/ (slice 003 --to-runtime).`);
|
|
319
|
+
}
|
|
320
|
+
if ((result.toRuntimeConflicts?.length ?? 0) > 0) {
|
|
321
|
+
nextActions.push(`${result.toRuntimeConflicts?.length} --to-runtime conflict(s) — see response. ${plans.filter((p) => p.action === 'f15-conflict-project-scan').length} are F15 carve-outs (deferred to a separate slice).`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
const wouldMoveCount = plans.filter((p) => p.action === 'moved').length;
|
|
326
|
+
const wouldSkipCount = plans.filter((p) => p.action === 'skipped-already-canonical').length;
|
|
327
|
+
if (wouldMoveCount > 0) {
|
|
328
|
+
nextActions.push(`Re-run with --apply to move ${wouldMoveCount} top-level session dir(s) to .peaks/_runtime/; ${wouldSkipCount} already canonical.`);
|
|
329
|
+
}
|
|
330
|
+
else if (wouldSkipCount > 0) {
|
|
331
|
+
nextActions.push(`All ${wouldSkipCount} top-level session dir(s) are already canonical — no moves needed.`);
|
|
332
|
+
}
|
|
333
|
+
const f15Count = plans.filter((p) => p.action === 'f15-conflict-project-scan').length;
|
|
334
|
+
if (f15Count > 0) {
|
|
335
|
+
nextActions.push(`${f15Count} F15 carve-out conflict(s) (rd/project-scan.md differs from runtime copy) — see response.`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (apply) {
|
|
340
|
+
if (result.moved.length > 0) {
|
|
341
|
+
nextActions.push(`Migrated ${result.moved.length} file(s) into .peaks/retrospective/.`);
|
|
342
|
+
}
|
|
343
|
+
if (result.deletedSessions.length > 0) {
|
|
344
|
+
nextActions.push(`Deleted ${result.deletedSessions.length} emptied session dir(s).`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
printResult(io, ok('workspace.migrate', result, warnings, nextActions), options.json ?? false);
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
printResult(io, fail('workspace.migrate', 'WORKSPACE_MIGRATE_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Verify the project path exists and is writable']), options.json ?? false);
|
|
351
|
+
process.exitCode = 1;
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Resolve the first-time "install peaks hooks" decision for this project.
|
|
357
|
+
* Decision tree:
|
|
358
|
+
*
|
|
359
|
+
* 1. Read the sticky marker.
|
|
360
|
+
* - Marker present:
|
|
361
|
+
* - marker.decision === 'installed' AND hooks are present → action: marker-honored, no side effects
|
|
362
|
+
* - marker.decision === 'installed' AND hooks are MISSING → re-install, action: reinstalled
|
|
363
|
+
* - marker.decision === 'skipped' → action: marker-honored, no install
|
|
364
|
+
* - Marker absent:
|
|
365
|
+
* - hooks already present → write a fresh 'installed' marker, action: already-installed
|
|
366
|
+
* - otherwise:
|
|
367
|
+
* - explicit --install-hooks=auto → install + marker, action: first-decision
|
|
368
|
+
* - explicit --install-hooks=skip → marker only, action: first-decision
|
|
369
|
+
* - explicit --install-hooks=ask OR default in TTY:
|
|
370
|
+
* - jsonMode → silently auto-install (LLM cannot answer), action: first-decision
|
|
371
|
+
* - TTY → prompt; on yes install + marker, on no marker-only
|
|
372
|
+
* - default in non-TTY → auto-install, action: first-decision
|
|
373
|
+
*
|
|
374
|
+
* Project scope is the only supported scope here; global scope is reserved
|
|
375
|
+
* for explicit `peaks hooks install --global` invocations.
|
|
376
|
+
*/
|
|
377
|
+
export async function resolveFirstTimeHooksInstall(options) {
|
|
378
|
+
const { projectRoot, jsonMode } = options;
|
|
379
|
+
const existingMarker = readDecisionMarker(projectRoot);
|
|
380
|
+
// readHookStatus can throw (e.g. .claude is a symlink → safety check rejects).
|
|
381
|
+
// Treat any throw as "hooks status unknown → treat as not-installed" so the
|
|
382
|
+
// function still reaches the install path; the install will surface the same
|
|
383
|
+
// error in a more specific reason field.
|
|
384
|
+
let hookStatus;
|
|
385
|
+
try {
|
|
386
|
+
hookStatus = readHookStatus('project', projectRoot);
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
hookStatus = { installed: false };
|
|
390
|
+
// Fall through to the install path; the failure will be captured below.
|
|
391
|
+
void error;
|
|
392
|
+
}
|
|
393
|
+
if (existingMarker !== null) {
|
|
394
|
+
if (existingMarker.decision === 'installed' && !hookStatus.installed) {
|
|
395
|
+
try {
|
|
396
|
+
applyHookInstall('project', projectRoot);
|
|
397
|
+
return { decision: 'installed', action: 'reinstalled', scope: 'project', reason: 'marker-said-installed-hooks-missing' };
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
return { decision: existingMarker.decision, action: 'marker-honored', scope: 'project', reason: `reinstall-failed: ${getErrorMessage(error)}` };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return { decision: existingMarker.decision, action: 'marker-honored', scope: existingMarker.scope };
|
|
404
|
+
}
|
|
405
|
+
// No marker yet — first decision.
|
|
406
|
+
if (hookStatus.installed) {
|
|
407
|
+
writeDecisionMarker(projectRoot, 'installed');
|
|
408
|
+
return { decision: 'installed', action: 'already-installed', scope: 'project' };
|
|
409
|
+
}
|
|
410
|
+
// Determine effective mode (explicit flag wins; default depends on TTY + jsonMode).
|
|
411
|
+
const explicitMode = options.explicitMode;
|
|
412
|
+
const effectiveMode = explicitMode ??
|
|
413
|
+
(jsonMode ? 'auto' : (process.stdin.isTTY === true ? 'ask' : 'auto'));
|
|
414
|
+
if (effectiveMode === 'skip') {
|
|
415
|
+
writeDecisionMarker(projectRoot, 'skipped');
|
|
416
|
+
return { decision: 'skipped', action: 'first-decision', scope: 'project', reason: 'explicit-skip' };
|
|
417
|
+
}
|
|
418
|
+
if (effectiveMode === 'auto' || jsonMode) {
|
|
419
|
+
// The reason code distinguishes the path the user took to reach auto-install:
|
|
420
|
+
// - explicit-auto: user passed --install-hooks=auto
|
|
421
|
+
// - json-mode: no --install-hooks flag, but --json was set
|
|
422
|
+
// - non-tty-default: no flag, no --json, stdin is not a TTY
|
|
423
|
+
let autoReason;
|
|
424
|
+
if (explicitMode === 'auto') {
|
|
425
|
+
autoReason = 'explicit-auto';
|
|
426
|
+
}
|
|
427
|
+
else if (jsonMode) {
|
|
428
|
+
autoReason = 'json-mode';
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
autoReason = 'non-tty-default';
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
applyHookInstall('project', projectRoot);
|
|
435
|
+
writeDecisionMarker(projectRoot, 'installed');
|
|
436
|
+
return { decision: 'installed', action: 'first-decision', scope: 'project', reason: autoReason };
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
// Auto-install failed: still record the decision so we do not keep retrying
|
|
440
|
+
// every workspace init. The user can fix the underlying problem and run
|
|
441
|
+
// `peaks hooks install` manually.
|
|
442
|
+
writeDecisionMarker(projectRoot, 'installed');
|
|
443
|
+
return { decision: 'installed', action: 'first-decision', scope: 'project', reason: `install-failed: ${getErrorMessage(error)}` };
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// effectiveMode === 'ask' AND TTY: prompt once.
|
|
447
|
+
process.stderr.write('\nPeaks-Cli: install the PreToolUse hooks for this project now?\n' +
|
|
448
|
+
' → Bash matcher: `peaks gate enforce` (SOP gate enforcement)\n' +
|
|
449
|
+
' → Task matcher: `peaks progress start` (auto-spawn sub-agent progress terminal)\n' +
|
|
450
|
+
'Both run on every Claude Code tool call without further prompting. The decision is sticky\n' +
|
|
451
|
+
'(recorded in .peaks/.peaks-init-hooks-decision.json) and re-runs of `workspace init` will\n' +
|
|
452
|
+
'honour it. Re-run with --install-hooks=skip or --install-hooks=auto to override.\n\n' +
|
|
453
|
+
'Install now? [Y/n]: ');
|
|
454
|
+
const answer = await promptYesNo('');
|
|
455
|
+
if (answer === null) {
|
|
456
|
+
// TTY disappeared mid-prompt (rare): treat as skip + write marker.
|
|
457
|
+
writeDecisionMarker(projectRoot, 'skipped');
|
|
458
|
+
return { decision: 'skipped', action: 'first-decision', scope: 'project', reason: 'tty-prompt-aborted' };
|
|
459
|
+
}
|
|
460
|
+
if (!answer) {
|
|
461
|
+
writeDecisionMarker(projectRoot, 'skipped');
|
|
462
|
+
return { decision: 'skipped', action: 'first-decision', scope: 'project', reason: 'user-answered-no' };
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
applyHookInstall('project', projectRoot);
|
|
466
|
+
writeDecisionMarker(projectRoot, 'installed');
|
|
467
|
+
return { decision: 'installed', action: 'first-decision', scope: 'project', reason: 'user-answered-yes' };
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
writeDecisionMarker(projectRoot, 'installed');
|
|
471
|
+
return { decision: 'installed', action: 'first-decision', scope: 'project', reason: `install-failed: ${getErrorMessage(error)}` };
|
|
472
|
+
}
|
|
136
473
|
}
|
package/dist/src/cli/program.js
CHANGED
|
@@ -15,6 +15,7 @@ import { registerProjectCommands } from './commands/project-commands.js';
|
|
|
15
15
|
import { registerRequestCommands } from './commands/request-commands.js';
|
|
16
16
|
import { registerScanCommands } from './commands/scan-commands.js';
|
|
17
17
|
import { registerShadcnCommands } from './commands/shadcn-commands.js';
|
|
18
|
+
import { registerSliceCommands } from './commands/slice-commands.js';
|
|
18
19
|
import { registerSopCommands } from './commands/sop-commands.js';
|
|
19
20
|
import { registerGateCommands } from './commands/gate-commands.js';
|
|
20
21
|
import { registerHooksCommands } from './commands/hooks-commands.js';
|
|
@@ -31,6 +32,8 @@ export function createProgram(io = { stdout: (text) => console.log(text), stderr
|
|
|
31
32
|
Run peaks (no arguments) for a quickstart. You likely want one of:
|
|
32
33
|
peaks doctor check your environment
|
|
33
34
|
peaks skill list or manage skills
|
|
35
|
+
peaks slice boundary check (tsc + vitest + 3-way + verify-pipeline)
|
|
36
|
+
peaks workflow plan workflow routing dry-run graphs
|
|
34
37
|
peaks sop author your own workflow gates
|
|
35
38
|
peaks hooks install the un-bypassable gate-enforcement hook
|
|
36
39
|
peaks gate enforce/bypass SOP gates on Bash commands`)
|
|
@@ -87,6 +90,7 @@ Run peaks (no arguments) for a quickstart. You likely want one of:
|
|
|
87
90
|
registerRequestCommands(program, io);
|
|
88
91
|
registerScanCommands(program, io);
|
|
89
92
|
registerShadcnCommands(program, io);
|
|
93
|
+
registerSliceCommands(program, io);
|
|
90
94
|
registerSopCommands(program, io);
|
|
91
95
|
registerGateCommands(program, io);
|
|
92
96
|
registerHooksCommands(program, io);
|
|
@@ -10,6 +10,15 @@ export type ArtifactPrerequisite = {
|
|
|
10
10
|
description: string;
|
|
11
11
|
/** Optional content markers — when set, the file must contain ALL of these (case-insensitive substring). */
|
|
12
12
|
mustContain?: ReadonlyArray<string>;
|
|
13
|
+
/**
|
|
14
|
+
* Optional content markers — when set, the file must contain AT LEAST ONE of
|
|
15
|
+
* these (case-insensitive substring). Use this for escape-hatch patterns
|
|
16
|
+
* (e.g. perf-baseline's "Results table" OR "N/A — no perf surface" stub).
|
|
17
|
+
* `mustContain` and `mustContainAny` are independent: when both are set,
|
|
18
|
+
* `mustContain` markers must all be present AND at least one `mustContainAny`
|
|
19
|
+
* marker must be present.
|
|
20
|
+
*/
|
|
21
|
+
mustContainAny?: ReadonlyArray<string>;
|
|
13
22
|
};
|
|
14
23
|
export type PrerequisiteCheckResult = {
|
|
15
24
|
ok: boolean;
|
|
@@ -20,7 +29,26 @@ export type PrerequisiteCheckResult = {
|
|
|
20
29
|
};
|
|
21
30
|
export type CheckPrerequisitesOptions = {
|
|
22
31
|
projectRoot: string;
|
|
23
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Durable scope of the artifact (the `.peaks/<changeId>/` directory
|
|
34
|
+
* the file lives in). The gate scans under `.peaks/<changeId>/<role>/`
|
|
35
|
+
* for prerequisite artifacts. As of slice 2026-06-05-change-id-as-unit-of-work,
|
|
36
|
+
* this replaces the legacy `sessionId` field — the file body and the
|
|
37
|
+
* on-disk path now agree on the same top-level dir.
|
|
38
|
+
*/
|
|
39
|
+
changeId: string;
|
|
40
|
+
/**
|
|
41
|
+
* Session binding (the developer's local session that wrote the
|
|
42
|
+
* request artifact). Read from the file body's `- session:` line.
|
|
43
|
+
* Optional, but when present the gate falls back to
|
|
44
|
+
* `.peaks/_runtime/<sid>/<role>/` and then `.peaks/<sid>/<role>/`
|
|
45
|
+
* for prerequisite artifacts that don't exist at the per-change-id
|
|
46
|
+
* path. This mirrors the F1/F2 back-compat pattern (read new path
|
|
47
|
+
* first, then legacy) and keeps the gate working for users whose
|
|
48
|
+
* QA / tech-doc / initiated artifacts still live under the session
|
|
49
|
+
* dir rather than under the change-id dir.
|
|
50
|
+
*/
|
|
51
|
+
sessionId?: string;
|
|
24
52
|
role: RequestArtifactRole;
|
|
25
53
|
newState: RequestArtifactState;
|
|
26
54
|
requestId: string;
|
|
@@ -30,6 +30,19 @@ const CODE_REVIEW = {
|
|
|
30
30
|
mustContain: ['## Findings', 'CRITICAL']
|
|
31
31
|
};
|
|
32
32
|
const SECURITY_REVIEW = { relativePath: 'rd/security-review.md', description: 'Security review evidence for the changed surface' };
|
|
33
|
+
// Gate B9 — RD-side perf baseline (peaks-rd SKILL "Parallel review fan-out").
|
|
34
|
+
// The file must exist; the body must either carry a Results table marker
|
|
35
|
+
// (per peaks-rd SKILL "Mandatory perf-baseline output") or the explicit
|
|
36
|
+
// "N/A — no perf surface" escape hatch. A slice without a perf surface
|
|
37
|
+
// still has to write the stub; an RD that omits perf-baseline entirely is
|
|
38
|
+
// blocked here, matching peaks-rd's BLOCKING Gate B9 claim.
|
|
39
|
+
const PERF_BASELINE = {
|
|
40
|
+
relativePath: 'rd/perf-baseline.md',
|
|
41
|
+
description: 'RD-side perf baseline (peaks-rd Gate B9) — must include a Results table with measurements OR the literal "N/A — no perf surface" escape hatch in the Notes section. QA Gate A4 diffs against this file.',
|
|
42
|
+
// Either a real Results table is present, OR the explicit no-perf-surface
|
|
43
|
+
// stub marker. Both paths satisfy Gate B9; absence of both is BLOCKED.
|
|
44
|
+
mustContainAny: ['## Results', 'N/A — no perf surface']
|
|
45
|
+
};
|
|
33
46
|
const TEST_CASES = {
|
|
34
47
|
relativePath: 'qa/test-cases/<rid>.md',
|
|
35
48
|
description: 'Generated test cases (unit / integration / UI regression)',
|
|
@@ -70,16 +83,19 @@ const QA_INITIATED = {
|
|
|
70
83
|
const FEATURE_TABLE = {
|
|
71
84
|
'prd:handed-off': [PRD_CONTENT],
|
|
72
85
|
'rd:implemented': [TECH_DOC],
|
|
73
|
-
'rd:qa-handoff': [TECH_DOC, CODE_REVIEW, SECURITY_REVIEW, UNIT_TESTS, QA_INITIATED],
|
|
86
|
+
'rd:qa-handoff': [TECH_DOC, CODE_REVIEW, SECURITY_REVIEW, PERF_BASELINE, UNIT_TESTS, QA_INITIATED],
|
|
74
87
|
'qa:running': [TEST_CASES],
|
|
75
88
|
'qa:verdict-issued': [TEST_CASES, TEST_REPORT, SECURITY_FINDINGS, PERFORMANCE_FINDINGS]
|
|
76
89
|
};
|
|
77
90
|
// Bugfix: lighter planning artifact (bug-analysis instead of tech-doc), still requires code review + security review + regression test.
|
|
78
|
-
// Performance
|
|
91
|
+
// Performance baseline: required for perf-shaped bugfixes (where the bug IS a
|
|
92
|
+
// perf regression). For non-perf bugfixes, RD writes the perf-baseline stub
|
|
93
|
+
// with "N/A — no perf surface" — Gate B9 still passes (mustContainAny hit),
|
|
94
|
+
// and the stub tells QA Gate A4 to skip the perf diff.
|
|
79
95
|
const BUGFIX_TABLE = {
|
|
80
96
|
'prd:handed-off': [PRD_CONTENT],
|
|
81
97
|
'rd:implemented': [BUG_ANALYSIS],
|
|
82
|
-
'rd:qa-handoff': [BUG_ANALYSIS, CODE_REVIEW, SECURITY_REVIEW, UNIT_TESTS, QA_INITIATED],
|
|
98
|
+
'rd:qa-handoff': [BUG_ANALYSIS, CODE_REVIEW, SECURITY_REVIEW, PERF_BASELINE, UNIT_TESTS, QA_INITIATED],
|
|
83
99
|
'qa:running': [TEST_CASES],
|
|
84
100
|
'qa:verdict-issued': [TEST_CASES, TEST_REPORT, SECURITY_FINDINGS]
|
|
85
101
|
};
|
|
@@ -146,11 +162,25 @@ export async function checkPrerequisites(options) {
|
|
|
146
162
|
if (requirements.length === 0) {
|
|
147
163
|
return { ok: true, missing: [] };
|
|
148
164
|
}
|
|
149
|
-
|
|
165
|
+
// Slice 006 simplifies the resolution to a 2-tier fallback. The
|
|
166
|
+
// per-change-id scope (`.peaks/<changeId>/<role>/`) is gone — new
|
|
167
|
+
// artifacts go to the session dir directly. The 2 tiers are:
|
|
168
|
+
// 1. `.peaks/_runtime/<sid>/<role>/...` (post-F3 canonical
|
|
169
|
+
// session home; primary).
|
|
170
|
+
// 2. `.peaks/<sid>/<role>/...` (pre-F3 legacy session home;
|
|
171
|
+
// back-compat).
|
|
172
|
+
// The changeId is preserved in the artifact body's frontmatter for
|
|
173
|
+
// human navigation; it is no longer a filesystem path key.
|
|
174
|
+
const canonicalSessionRoot = options.sessionId !== undefined
|
|
175
|
+
? join(options.projectRoot, '.peaks', '_runtime', options.sessionId)
|
|
176
|
+
: null;
|
|
177
|
+
const legacySessionRoot = options.sessionId !== undefined
|
|
178
|
+
? join(options.projectRoot, '.peaks', options.sessionId)
|
|
179
|
+
: null;
|
|
150
180
|
const missing = [];
|
|
151
181
|
for (const prerequisite of requirements) {
|
|
152
182
|
const relative = resolvePrerequisitePath(prerequisite, options.requestId);
|
|
153
|
-
const absolute = await
|
|
183
|
+
const absolute = await resolvePrerequisiteAbsolutePathWithFallback(canonicalSessionRoot, legacySessionRoot, prerequisite, options.requestId);
|
|
154
184
|
if (absolute === null) {
|
|
155
185
|
missing.push({ path: relative, description: prerequisite.description });
|
|
156
186
|
continue;
|
|
@@ -166,6 +196,40 @@ export async function checkPrerequisites(options) {
|
|
|
166
196
|
});
|
|
167
197
|
}
|
|
168
198
|
}
|
|
199
|
+
if (prerequisite.mustContainAny && prerequisite.mustContainAny.length > 0) {
|
|
200
|
+
const body = await readFile(absolute, 'utf8');
|
|
201
|
+
const lowered = body.toLowerCase();
|
|
202
|
+
const hitAny = prerequisite.mustContainAny.some((marker) => lowered.includes(marker.toLowerCase()));
|
|
203
|
+
if (!hitAny) {
|
|
204
|
+
missing.push({
|
|
205
|
+
path: relative,
|
|
206
|
+
description: `${prerequisite.description} — none of the escape-hatch markers present: ${prerequisite.mustContainAny.join(', ')}`
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
169
210
|
}
|
|
170
211
|
return { ok: missing.length === 0, missing };
|
|
171
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Resolve a prerequisite to an on-disk path, with a 2-tier fallback
|
|
215
|
+
* (slice 006 — the per-change-id tier was dropped because per-change-id
|
|
216
|
+
* dirs are no longer created):
|
|
217
|
+
* 1. `<canonicalSessionRoot>/<relative>` (post-F3 canonical session
|
|
218
|
+
* home, when `canonicalSessionRoot` is provided).
|
|
219
|
+
* 2. `<legacySessionRoot>/<relative>` (pre-F3 legacy session home,
|
|
220
|
+
* when `legacySessionRoot` is provided).
|
|
221
|
+
* Tolerates the numbered filename prefix that `request init` writes
|
|
222
|
+
* (e.g. `001-<rid>.md`) at every tier. Returns the matched absolute
|
|
223
|
+
* path, or null when nothing matches.
|
|
224
|
+
*/
|
|
225
|
+
async function resolvePrerequisiteAbsolutePathWithFallback(canonicalSessionRoot, legacySessionRoot, prerequisite, requestId) {
|
|
226
|
+
const roots = [canonicalSessionRoot, legacySessionRoot];
|
|
227
|
+
for (const root of roots) {
|
|
228
|
+
if (root === null)
|
|
229
|
+
continue;
|
|
230
|
+
const found = await resolvePrerequisiteAbsolutePath(root, prerequisite, requestId);
|
|
231
|
+
if (found !== null)
|
|
232
|
+
return found;
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
@@ -6,6 +6,14 @@ export type CreateRequestArtifactOptions = {
|
|
|
6
6
|
requestId: string;
|
|
7
7
|
projectRoot: string;
|
|
8
8
|
sessionId?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Optional explicit change-id. When set, the artifact file lands at
|
|
11
|
+
* `.peaks/<changeId>/<role>/requests/...` regardless of any
|
|
12
|
+
* `current-change` binding. When unset, falls back to the binding, then
|
|
13
|
+
* to the requestId. The CLI's `--session-id <scope>` flag uses this to
|
|
14
|
+
* preserve the legacy "session-id as scope dir name" behavior.
|
|
15
|
+
*/
|
|
16
|
+
changeId?: string;
|
|
9
17
|
apply?: boolean;
|
|
10
18
|
requestType?: RequestType;
|
|
11
19
|
clock?: () => string;
|
|
@@ -21,6 +29,20 @@ export type CreateRequestArtifactResult = {
|
|
|
21
29
|
export declare function createRequestArtifact(options: CreateRequestArtifactOptions): Promise<CreateRequestArtifactResult>;
|
|
22
30
|
export type RequestArtifactSummary = {
|
|
23
31
|
role: RequestArtifactRole;
|
|
32
|
+
/**
|
|
33
|
+
* Durable scope of the artifact: the top-level `.peaks/<changeId>/`
|
|
34
|
+
* directory the file lives in. As of slice 2026-06-05-change-id-as-unit-of-work,
|
|
35
|
+
* the prerequisite gate resolves paths under this dir (not the body
|
|
36
|
+
* `- session:` line), so the file body and the on-disk path agree.
|
|
37
|
+
*/
|
|
38
|
+
changeId: string;
|
|
39
|
+
/**
|
|
40
|
+
* Session binding (which developer's local session wrote the file).
|
|
41
|
+
* Read from the file body's `- session:` line. Falls back to `changeId`
|
|
42
|
+
* when the body is missing the line. For back-compat with legacy
|
|
43
|
+
* session-id dirs, this may equal the dir name; for new change-id
|
|
44
|
+
* dirs, it is the metadata session that produced the file.
|
|
45
|
+
*/
|
|
24
46
|
sessionId: string;
|
|
25
47
|
requestId: string;
|
|
26
48
|
path: string;
|