peaks-cli 1.0.2 → 1.0.4
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/dist/src/cli/commands/config-commands.js +3 -2
- package/dist/src/cli/commands/sc-commands.js +1 -1
- package/dist/src/cli/commands/workflow-commands.js +13 -2
- package/dist/src/services/artifacts/artifact-service.js +5 -4
- package/dist/src/services/artifacts/workspace-service.d.ts +1 -0
- package/dist/src/services/artifacts/workspace-service.js +56 -28
- package/dist/src/services/config/config-service.d.ts +2 -0
- package/dist/src/services/config/config-service.js +139 -12
- package/dist/src/services/config/config-types.d.ts +16 -5
- package/dist/src/services/rd/rd-service.js +16 -14
- package/dist/src/services/refactor/refactor-service.js +7 -4
- package/dist/src/services/sc/sc-service.d.ts +2 -1
- package/dist/src/services/sc/sc-service.js +65 -36
- package/dist/src/services/tech/tech-service.js +59 -15
- package/dist/src/services/workflow/workflow-autonomous-service.js +31 -8
- package/dist/src/shared/change-id.js +1 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/output-styles/peaks-skill-swarm.md +132 -0
- package/package.json +4 -1
- package/schemas/artifact-retention-report.schema.json +31 -10
- package/scripts/install-skills.mjs +65 -8
- package/skills/peaks-prd/SKILL.md +59 -0
- package/skills/peaks-prd/references/artifact-contracts.md +4 -0
- package/skills/peaks-prd/references/workflow.md +29 -0
- package/skills/peaks-qa/SKILL.md +44 -2
- package/skills/peaks-qa/references/artifact-contracts.md +4 -0
- package/skills/peaks-qa/references/regression-gates.md +9 -1
- package/skills/peaks-rd/SKILL.md +78 -2
- package/skills/peaks-rd/references/artifact-contracts.md +4 -0
- package/skills/peaks-rd/references/refactor-workflow.md +11 -3
- package/skills/peaks-sc/SKILL.md +11 -3
- package/skills/peaks-sc/references/artifact-retention.md +3 -3
- package/skills/peaks-solo/SKILL.md +54 -1
- package/skills/peaks-solo/references/artifact-contracts.md +4 -0
- package/skills/peaks-solo/references/refactor-mode.md +2 -2
- package/skills/peaks-solo/references/workflow.md +18 -0
- package/skills/peaks-txt/SKILL.md +28 -1
- package/skills/peaks-txt/references/artifact-contracts.md +4 -0
- package/skills/peaks-txt/references/context-capsule.md +2 -1
- package/skills/peaks-ui/SKILL.md +25 -0
- package/skills/peaks-ui/references/workflow.md +17 -1
|
@@ -98,7 +98,9 @@ function readArtifactFile(rootPath, artifactWorkspacePath, artifact) {
|
|
|
98
98
|
try {
|
|
99
99
|
const artifactWorkspaceRealPath = stableRealPath(artifactWorkspacePath);
|
|
100
100
|
const rootRealPath = stableRealPath(rootPath);
|
|
101
|
-
|
|
101
|
+
const rdRootPath = resolve(rootPath, '..');
|
|
102
|
+
const sessionRootPath = resolve(rdRootPath, '..');
|
|
103
|
+
if (lstatSync(sessionRootPath).isSymbolicLink() || lstatSync(rdRootPath).isSymbolicLink() || lstatSync(rootPath).isSymbolicLink() || !isInsidePath(rootRealPath, artifactWorkspaceRealPath)) {
|
|
102
104
|
return null;
|
|
103
105
|
}
|
|
104
106
|
const artifactStat = lstatSync(artifactPath);
|
|
@@ -138,7 +140,7 @@ function getConcreteTargetAreas(request, artifactWorkspacePath, hasApprovedTechA
|
|
|
138
140
|
if (!artifactWorkspacePath || !hasApprovedTechArtifacts || !hasPlannerArtifactWorkspace(request, artifactWorkspacePath)) {
|
|
139
141
|
return [];
|
|
140
142
|
}
|
|
141
|
-
const architectureRoot = join(artifactWorkspacePath, '.peaks',
|
|
143
|
+
const architectureRoot = join(artifactWorkspacePath, '.peaks', request.changeId, 'rd', 'architecture');
|
|
142
144
|
const candidates = TECH_REQUIRED_ARTIFACTS.flatMap((artifact) => {
|
|
143
145
|
if (artifact === 'tech-approval-record.md') {
|
|
144
146
|
return [];
|
|
@@ -155,7 +157,7 @@ function buildPlan(request) {
|
|
|
155
157
|
const executionModelId = request.executionModelId?.trim() || getConfiguredExecutionModelId(undefined);
|
|
156
158
|
const { workerTarget, blockedReasons } = resolveWorkerTarget(request.maxWorkers);
|
|
157
159
|
const artifactWorkspacePath = resolveArtifactWorkspacePath(request);
|
|
158
|
-
const artifactRoot = buildArtifactRelativePath(request.changeId, 'swarm');
|
|
160
|
+
const artifactRoot = buildArtifactRelativePath(request.changeId, 'rd', 'swarm');
|
|
159
161
|
const techStatus = getTechStatus({
|
|
160
162
|
changeId: request.changeId,
|
|
161
163
|
...(artifactWorkspacePath ? { artifactWorkspacePath } : {}),
|
|
@@ -174,10 +176,10 @@ function buildPlan(request) {
|
|
|
174
176
|
conflictGroups: [],
|
|
175
177
|
artifactRoot,
|
|
176
178
|
outputs: {
|
|
177
|
-
taskGraph: buildArtifactRelativePath(request.changeId, 'swarm', 'task-graph.json'),
|
|
179
|
+
taskGraph: buildArtifactRelativePath(request.changeId, 'rd', 'swarm', 'task-graph.json'),
|
|
178
180
|
waveManifests: [],
|
|
179
181
|
workerBriefs: [],
|
|
180
|
-
reducerReport: buildArtifactRelativePath(request.changeId, 'swarm', 'reducer-report.md'),
|
|
182
|
+
reducerReport: buildArtifactRelativePath(request.changeId, 'rd', 'swarm', 'reducer-report.md'),
|
|
181
183
|
},
|
|
182
184
|
gateStatus: {
|
|
183
185
|
techApprovalRequired: requiresTechApproval,
|
|
@@ -199,10 +201,10 @@ function buildPlan(request) {
|
|
|
199
201
|
conflictGroups: [],
|
|
200
202
|
artifactRoot,
|
|
201
203
|
outputs: {
|
|
202
|
-
taskGraph: buildArtifactRelativePath(request.changeId, 'swarm', 'task-graph.json'),
|
|
204
|
+
taskGraph: buildArtifactRelativePath(request.changeId, 'rd', 'swarm', 'task-graph.json'),
|
|
203
205
|
waveManifests: [],
|
|
204
206
|
workerBriefs: [],
|
|
205
|
-
reducerReport: buildArtifactRelativePath(request.changeId, 'swarm', 'reducer-report.md'),
|
|
207
|
+
reducerReport: buildArtifactRelativePath(request.changeId, 'rd', 'swarm', 'reducer-report.md'),
|
|
206
208
|
},
|
|
207
209
|
gateStatus: {
|
|
208
210
|
techApprovalRequired: true,
|
|
@@ -223,10 +225,10 @@ function buildPlan(request) {
|
|
|
223
225
|
conflictGroups: [],
|
|
224
226
|
artifactRoot,
|
|
225
227
|
outputs: {
|
|
226
|
-
taskGraph: buildArtifactRelativePath(request.changeId, 'swarm', 'task-graph.json'),
|
|
228
|
+
taskGraph: buildArtifactRelativePath(request.changeId, 'rd', 'swarm', 'task-graph.json'),
|
|
227
229
|
waveManifests: [],
|
|
228
230
|
workerBriefs: [],
|
|
229
|
-
reducerReport: buildArtifactRelativePath(request.changeId, 'swarm', 'reducer-report.md'),
|
|
231
|
+
reducerReport: buildArtifactRelativePath(request.changeId, 'rd', 'swarm', 'reducer-report.md'),
|
|
230
232
|
},
|
|
231
233
|
gateStatus: {
|
|
232
234
|
techApprovalRequired: requiresTechApproval,
|
|
@@ -263,7 +265,7 @@ function buildPlan(request) {
|
|
|
263
265
|
};
|
|
264
266
|
const tasks = taskIds.map((taskId, index) => {
|
|
265
267
|
const wave = index < 8 ? 'discovery' : index < 16 ? 'planning' : index < taskIds.length - 8 ? 'implementation candidates' : index < taskIds.length - 5 ? 'unit-test execution' : index < taskIds.length - 1 ? 'quality gates' : 'reducer';
|
|
266
|
-
const briefPath = buildArtifactRelativePath(request.changeId, 'swarm', 'workers', taskId, 'brief.md');
|
|
268
|
+
const briefPath = buildArtifactRelativePath(request.changeId, 'rd', 'swarm', 'workers', taskId, 'brief.md');
|
|
267
269
|
const implementationIndex = index - 16;
|
|
268
270
|
const targetArea = wave === 'implementation candidates' && hasConcreteTargetAreas(concreteTargetAreas)
|
|
269
271
|
? selectConcreteTargetArea(concreteTargetAreas, implementationIndex)
|
|
@@ -294,7 +296,7 @@ function buildPlan(request) {
|
|
|
294
296
|
});
|
|
295
297
|
const conflictGroups = waves.map((wave) => ({
|
|
296
298
|
groupId: `group-${wave.name.replace(/\s+/g, '-')}`,
|
|
297
|
-
ownedPaths: wave.taskIds.map((taskId) => buildArtifactRelativePath(request.changeId, 'swarm', 'workers', taskId, 'brief.md')),
|
|
299
|
+
ownedPaths: wave.taskIds.map((taskId) => buildArtifactRelativePath(request.changeId, 'rd', 'swarm', 'workers', taskId, 'brief.md')),
|
|
298
300
|
parallelismPolicy: wave.taskIds.length > 1 ? 'parallel' : 'sequential',
|
|
299
301
|
reason: `${wave.name} work is isolated by worker output path`,
|
|
300
302
|
}));
|
|
@@ -308,10 +310,10 @@ function buildPlan(request) {
|
|
|
308
310
|
conflictGroups,
|
|
309
311
|
artifactRoot,
|
|
310
312
|
outputs: {
|
|
311
|
-
taskGraph: buildArtifactRelativePath(request.changeId, 'swarm', 'task-graph.json'),
|
|
312
|
-
waveManifests: waves.map((wave, index) => buildArtifactRelativePath(request.changeId, 'swarm', 'waves', `wave-${index + 1}-${wave.name}.json`)),
|
|
313
|
+
taskGraph: buildArtifactRelativePath(request.changeId, 'rd', 'swarm', 'task-graph.json'),
|
|
314
|
+
waveManifests: waves.map((wave, index) => buildArtifactRelativePath(request.changeId, 'rd', 'swarm', 'waves', `wave-${index + 1}-${wave.name}.json`)),
|
|
313
315
|
workerBriefs: tasks.map((task) => task.outputs[0]),
|
|
314
|
-
reducerReport: buildArtifactRelativePath(request.changeId, 'swarm', 'reducer-report.md'),
|
|
316
|
+
reducerReport: buildArtifactRelativePath(request.changeId, 'rd', 'swarm', 'reducer-report.md'),
|
|
315
317
|
},
|
|
316
318
|
gateStatus: {
|
|
317
319
|
techApprovalRequired: requiresTechApproval,
|
|
@@ -12,7 +12,8 @@ export function createRefactorDryRun(mode) {
|
|
|
12
12
|
'Generate strict verifiable spec before each slice',
|
|
13
13
|
'Require peaks-prd and peaks-qa artifacts even for direct peaks-rd refactor',
|
|
14
14
|
'Require 100% acceptance for each slice',
|
|
15
|
-
'
|
|
15
|
+
'Retain code changes and intermediate artifacts in local .peaks/<session-id>/ storage before the next slice',
|
|
16
|
+
'Commit or sync artifacts only after explicit authorization'
|
|
16
17
|
],
|
|
17
18
|
requiredArtifacts: [
|
|
18
19
|
'project-scan.md',
|
|
@@ -20,13 +21,15 @@ export function createRefactorDryRun(mode) {
|
|
|
20
21
|
'feature-slice-map.md',
|
|
21
22
|
'slice-spec.md',
|
|
22
23
|
'acceptance-spec.md',
|
|
24
|
+
'code-review-report.md',
|
|
25
|
+
'security-review-report.md',
|
|
26
|
+
'post-check-dry-run.md',
|
|
23
27
|
'validation-report.md',
|
|
24
|
-
'
|
|
25
|
-
'commit-required.md'
|
|
28
|
+
'retention-boundary.md'
|
|
26
29
|
],
|
|
27
30
|
nextActions: [
|
|
28
31
|
'Run doctor checks',
|
|
29
|
-
'
|
|
32
|
+
'Create or discover local .peaks/<session-id>/ artifact workspace',
|
|
30
33
|
'Generate the first refactor slice spec before implementation'
|
|
31
34
|
]
|
|
32
35
|
};
|
|
@@ -26,7 +26,8 @@ export type ArtifactRetentionReport = {
|
|
|
26
26
|
coverageArtifacts: string[];
|
|
27
27
|
reviewArtifacts: string[];
|
|
28
28
|
codeChanges: string[];
|
|
29
|
-
|
|
29
|
+
retentionStatus: 'local-ready' | 'pending' | 'explicitly-committed' | 'rolled-back';
|
|
30
|
+
commitHash: string | null;
|
|
30
31
|
rollbackPoint: string | null;
|
|
31
32
|
};
|
|
32
33
|
export type ChangeTraceabilityStatus = {
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { basename, relative, resolve } from 'node:path';
|
|
4
|
+
import { isInsidePath } from '../../shared/path-utils.js';
|
|
4
5
|
import { getCurrentWorkspaceConfig } from '../config/config-service.js';
|
|
5
|
-
import { getArtifactWorkspaceStatus, getLocalArtifactPath } from '../artifacts/workspace-service.js';
|
|
6
|
+
import { getArtifactRemoteRepo, getArtifactWorkspaceStatus, getLocalArtifactPath } from '../artifacts/workspace-service.js';
|
|
6
7
|
const REQUIRED_ARTIFACTS = [
|
|
7
|
-
{ name: '
|
|
8
|
+
{ name: 'retention-boundary.md', path: ['sc', 'retention-boundary.md'] },
|
|
8
9
|
{ name: 'change-impact.json', path: ['sc', 'change-impact.json'] },
|
|
9
|
-
{ name: '
|
|
10
|
-
{ name: 'coverage-report.md', path: ['qa', 'coverage-report.md'] }
|
|
10
|
+
{ name: 'coverage-report.md', path: ['rd', 'coverage-report.md'] }
|
|
11
11
|
];
|
|
12
12
|
const RETENTION_REQUIREMENTS = [
|
|
13
|
-
['
|
|
14
|
-
['
|
|
13
|
+
['prd', 'refactor-goal.md'],
|
|
14
|
+
['rd', 'slice-spec.md'],
|
|
15
|
+
['rd', 'coverage-report.md'],
|
|
16
|
+
['rd', 'code-review-report.md'],
|
|
17
|
+
['rd', 'security-review-report.md'],
|
|
18
|
+
['rd', 'post-check-dry-run.md'],
|
|
15
19
|
['qa', 'validation-report.md'],
|
|
16
|
-
['
|
|
17
|
-
['
|
|
20
|
+
['sc', 'change-impact.json'],
|
|
21
|
+
['sc', 'retention-boundary.md'],
|
|
22
|
+
['txt', 'context-capsule.md']
|
|
18
23
|
];
|
|
19
24
|
const SLICE_ID_PATTERN = /^(?!\.{1,2}$)[A-Za-z0-9._-]+$/;
|
|
20
25
|
function getPeaksPath(workspaceRoot) {
|
|
@@ -27,12 +32,16 @@ function resolveCurrentChangeId(peaksPath) {
|
|
|
27
32
|
try {
|
|
28
33
|
const stat = lstatSync(currentChangePath);
|
|
29
34
|
if (stat.isSymbolicLink()) {
|
|
30
|
-
|
|
35
|
+
const targetPath = realpathSync(currentChangePath);
|
|
36
|
+
if (!isInsidePath(targetPath, realpathSync(peaksPath)))
|
|
37
|
+
return null;
|
|
38
|
+
const targetId = basename(targetPath);
|
|
39
|
+
return SLICE_ID_PATTERN.test(targetId) ? targetId : null;
|
|
31
40
|
}
|
|
32
41
|
const raw = readFileSync(currentChangePath, 'utf-8').trim();
|
|
33
|
-
if (!raw)
|
|
42
|
+
if (!raw || !SLICE_ID_PATTERN.test(raw))
|
|
34
43
|
return null;
|
|
35
|
-
return
|
|
44
|
+
return raw;
|
|
36
45
|
}
|
|
37
46
|
catch {
|
|
38
47
|
return null;
|
|
@@ -63,24 +72,42 @@ function mapSyncState(syncStatus) {
|
|
|
63
72
|
return 'pending';
|
|
64
73
|
return 'failed';
|
|
65
74
|
}
|
|
66
|
-
function getCurrentArtifactDir(
|
|
67
|
-
const peaksPath = getPeaksPath(
|
|
75
|
+
function getCurrentArtifactDir(artifactWorkspacePath) {
|
|
76
|
+
const peaksPath = getPeaksPath(artifactWorkspacePath);
|
|
68
77
|
const changeId = resolveCurrentChangeId(peaksPath);
|
|
69
|
-
const effectiveChangeId = changeId ?? 'unknown-
|
|
78
|
+
const effectiveChangeId = changeId ?? 'unknown-session';
|
|
70
79
|
return {
|
|
71
80
|
peaksPath,
|
|
72
81
|
changeId,
|
|
73
|
-
changeDir: resolve(peaksPath,
|
|
82
|
+
changeDir: resolve(peaksPath, effectiveChangeId)
|
|
74
83
|
};
|
|
75
84
|
}
|
|
76
|
-
function getRetentionChangeDir(
|
|
77
|
-
const peaksPath = getPeaksPath(
|
|
85
|
+
function getRetentionChangeDir(artifactWorkspacePath, sliceId) {
|
|
86
|
+
const peaksPath = getPeaksPath(artifactWorkspacePath);
|
|
78
87
|
return {
|
|
79
88
|
peaksPath,
|
|
80
89
|
changeId: sliceId,
|
|
81
|
-
changeDir: resolve(peaksPath,
|
|
90
|
+
changeDir: resolve(peaksPath, sliceId)
|
|
82
91
|
};
|
|
83
92
|
}
|
|
93
|
+
function isRetainedArtifactFile(filePath, artifactWorkspacePath, changesRoot, changeDir) {
|
|
94
|
+
if (!existsSync(filePath))
|
|
95
|
+
return false;
|
|
96
|
+
try {
|
|
97
|
+
const artifactWorkspaceRealPath = realpathSync(artifactWorkspacePath);
|
|
98
|
+
const changesRootRealPath = realpathSync(changesRoot);
|
|
99
|
+
const changeDirRealPath = realpathSync(changeDir);
|
|
100
|
+
const fileRealPath = realpathSync(filePath);
|
|
101
|
+
return !lstatSync(changesRoot).isSymbolicLink()
|
|
102
|
+
&& !lstatSync(changeDir).isSymbolicLink()
|
|
103
|
+
&& isInsidePath(changesRootRealPath, artifactWorkspaceRealPath)
|
|
104
|
+
&& isInsidePath(changeDirRealPath, changesRootRealPath)
|
|
105
|
+
&& isInsidePath(fileRealPath, changeDirRealPath);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
84
111
|
export function getChangeTraceabilityStatus() {
|
|
85
112
|
const workspace = getCurrentWorkspaceConfig();
|
|
86
113
|
const artifactStatus = getArtifactWorkspaceStatus(workspace?.workspaceId);
|
|
@@ -92,31 +119,30 @@ export function getChangeTraceabilityStatus() {
|
|
|
92
119
|
localArtifactPath: '.peaks-artifacts',
|
|
93
120
|
requiredArtifacts: REQUIRED_ARTIFACTS.map((artifact) => ({
|
|
94
121
|
name: artifact.name,
|
|
95
|
-
path: resolve('.peaks', '
|
|
122
|
+
path: resolve('.peaks', '<session-id>', ...artifact.path),
|
|
96
123
|
exists: false
|
|
97
124
|
})),
|
|
98
125
|
nextActions: ['Add a workspace: peaks config workspace add --id <id> --name <name> --path <path>']
|
|
99
126
|
};
|
|
100
127
|
}
|
|
101
|
-
const
|
|
102
|
-
const
|
|
128
|
+
const artifactWorkspacePath = getLocalArtifactPath(workspace);
|
|
129
|
+
const { peaksPath, changeId, changeDir } = getCurrentArtifactDir(artifactWorkspacePath);
|
|
130
|
+
const artifactRepo = getArtifactRemoteRepo(workspace);
|
|
131
|
+
const hasArtifactRepo = Boolean(artifactRepo);
|
|
132
|
+
const changesRoot = peaksPath;
|
|
103
133
|
const requiredArtifacts = REQUIRED_ARTIFACTS.map((artifact) => {
|
|
104
134
|
const artifactPath = resolve(changeDir, ...artifact.path);
|
|
105
135
|
return {
|
|
106
136
|
name: artifact.name,
|
|
107
|
-
path: resolve(peaksPath,
|
|
108
|
-
exists:
|
|
137
|
+
path: resolve(peaksPath, changeId ?? '<session-id>', ...artifact.path),
|
|
138
|
+
exists: isRetainedArtifactFile(artifactPath, artifactWorkspacePath, changesRoot, changeDir)
|
|
109
139
|
};
|
|
110
140
|
});
|
|
111
141
|
const nextActions = [];
|
|
112
142
|
if (!changeId) {
|
|
113
143
|
nextActions.push('Set the current change in .peaks/current-change');
|
|
114
144
|
}
|
|
115
|
-
if (
|
|
116
|
-
nextActions.push('Configure artifact repo: peaks config workspace add --id <id> --provider github --repo-owner <owner> --repo-name <name>');
|
|
117
|
-
nextActions.push('Then run: peaks artifacts init --provider github --name <repo> --dry-run');
|
|
118
|
-
}
|
|
119
|
-
else if (artifactStatus.syncStatus === 'pending') {
|
|
145
|
+
if (hasArtifactRepo && artifactStatus.syncStatus === 'pending') {
|
|
120
146
|
nextActions.push(`Run peaks artifacts sync --workspace ${workspace.workspaceId} --dry-run`);
|
|
121
147
|
}
|
|
122
148
|
return {
|
|
@@ -130,7 +156,7 @@ export function getChangeTraceabilityStatus() {
|
|
|
130
156
|
}
|
|
131
157
|
export function createChangeImpact(options) {
|
|
132
158
|
const workspace = getCurrentWorkspaceConfig();
|
|
133
|
-
const artifactRepo = workspace
|
|
159
|
+
const artifactRepo = workspace ? getArtifactRemoteRepo(workspace) : null;
|
|
134
160
|
return {
|
|
135
161
|
changeId: options.changeId,
|
|
136
162
|
sourceArtifacts: options.sourceArtifacts ?? [],
|
|
@@ -161,7 +187,8 @@ export function createArtifactRetentionReport(options) {
|
|
|
161
187
|
coverageArtifacts: options.coverageArtifacts ?? [],
|
|
162
188
|
reviewArtifacts: options.reviewArtifacts ?? [],
|
|
163
189
|
codeChanges: options.codeChanges ?? [],
|
|
164
|
-
|
|
190
|
+
retentionStatus: 'pending',
|
|
191
|
+
commitHash: null,
|
|
165
192
|
rollbackPoint: null
|
|
166
193
|
};
|
|
167
194
|
}
|
|
@@ -192,13 +219,15 @@ export function validateArtifactRetention(sliceId) {
|
|
|
192
219
|
return {
|
|
193
220
|
valid: false,
|
|
194
221
|
missingArtifacts: ['Invalid slice id'],
|
|
195
|
-
warnings: ['Slice id must stay inside .peaks
|
|
222
|
+
warnings: ['Slice id must stay inside .peaks/<session-id> and only contain letters, numbers, dots, underscores, or hyphens']
|
|
196
223
|
};
|
|
197
224
|
}
|
|
198
|
-
const
|
|
225
|
+
const artifactWorkspacePath = getLocalArtifactPath(workspace);
|
|
226
|
+
const { peaksPath, changeDir } = getRetentionChangeDir(artifactWorkspacePath, sliceId);
|
|
227
|
+
const changesRoot = peaksPath;
|
|
199
228
|
const missingArtifacts = RETENTION_REQUIREMENTS
|
|
200
229
|
.map(([folder, file]) => resolve(changeDir, folder, file))
|
|
201
|
-
.filter((filePath) => !
|
|
230
|
+
.filter((filePath) => !isRetainedArtifactFile(filePath, artifactWorkspacePath, changesRoot, changeDir))
|
|
202
231
|
.map((filePath) => relative(changeDir, filePath).replace(/\\/g, '/'));
|
|
203
232
|
return {
|
|
204
233
|
valid: missingArtifacts.length === 0,
|
|
@@ -212,12 +241,12 @@ export function getScHelpText() {
|
|
|
212
241
|
'peaks sc impact --change-id <id> Generate change impact artifact',
|
|
213
242
|
'peaks sc retention --slice-id <id> Create artifact retention report',
|
|
214
243
|
'peaks sc validate --slice-id <id> Validate artifact retention',
|
|
215
|
-
'peaks sc boundary --slice-id <id> Record
|
|
244
|
+
'peaks sc boundary --slice-id <id> Record retention boundary for slice',
|
|
216
245
|
'',
|
|
217
246
|
'Change traceability workflow integration:',
|
|
218
247
|
' 1. Run peaks sc status to check current state',
|
|
219
248
|
' 2. After slice completion, run peaks sc retention --slice-id <id>',
|
|
220
|
-
' 3.
|
|
221
|
-
' 4. Commit
|
|
249
|
+
' 3. Keep artifacts local in .peaks/<session-id>/ by default',
|
|
250
|
+
' 4. Commit or sync artifacts only after explicit authorization'
|
|
222
251
|
];
|
|
223
252
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, lstatSync,
|
|
1
|
+
import { closeSync, existsSync, fstatSync, lstatSync, openSync, readSync, statSync } from 'node:fs';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { isInsidePath, stableRealPath } from '../../shared/path-utils.js';
|
|
4
4
|
import { buildArtifactRelativePath, validateChangeIdOrThrow } from '../../shared/change-id.js';
|
|
@@ -28,7 +28,7 @@ function assertNonEmptyGoal(goal) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
function architectureRoot(changeId) {
|
|
31
|
-
return buildArtifactRelativePath(changeId, 'architecture');
|
|
31
|
+
return buildArtifactRelativePath(changeId, 'rd', 'architecture');
|
|
32
32
|
}
|
|
33
33
|
function hasPlannerArtifactWorkspace(artifactWorkspacePath, workspace) {
|
|
34
34
|
return !!workspace && hasValidArtifactWorkspace(workspace, artifactWorkspacePath);
|
|
@@ -38,12 +38,59 @@ function isEscapedArchitectureRoot(rootPath, artifactWorkspacePath) {
|
|
|
38
38
|
return false;
|
|
39
39
|
}
|
|
40
40
|
try {
|
|
41
|
-
|
|
41
|
+
const rdRootPath = resolve(rootPath, '..');
|
|
42
|
+
const sessionRootPath = resolve(rdRootPath, '..');
|
|
43
|
+
return lstatSync(sessionRootPath).isSymbolicLink()
|
|
44
|
+
|| lstatSync(rdRootPath).isSymbolicLink()
|
|
45
|
+
|| lstatSync(rootPath).isSymbolicLink()
|
|
46
|
+
|| !isInsidePath(stableRealPath(rootPath), stableRealPath(artifactWorkspacePath));
|
|
42
47
|
}
|
|
43
48
|
catch {
|
|
44
49
|
return true;
|
|
45
50
|
}
|
|
46
51
|
}
|
|
52
|
+
const MAX_TECH_ARTIFACT_BYTES = 256_000;
|
|
53
|
+
function readTechArtifactFile(rootPath, artifact) {
|
|
54
|
+
const artifactPath = resolve(rootPath, artifact);
|
|
55
|
+
try {
|
|
56
|
+
const rootRealPath = stableRealPath(rootPath);
|
|
57
|
+
const artifactStat = lstatSync(artifactPath);
|
|
58
|
+
if (artifactStat.isSymbolicLink() || !artifactStat.isFile() || artifactStat.size > MAX_TECH_ARTIFACT_BYTES) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (!isInsidePath(stableRealPath(artifactPath), rootRealPath)) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const fd = openSync(artifactPath, 'r');
|
|
65
|
+
try {
|
|
66
|
+
const openedStat = fstatSync(fd);
|
|
67
|
+
const currentStat = statSync(artifactPath);
|
|
68
|
+
if (!openedStat.isFile() || openedStat.size > MAX_TECH_ARTIFACT_BYTES || openedStat.dev !== artifactStat.dev || openedStat.ino !== artifactStat.ino || openedStat.dev !== currentStat.dev || openedStat.ino !== currentStat.ino) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const buffer = Buffer.alloc(openedStat.size);
|
|
72
|
+
let offset = 0;
|
|
73
|
+
while (offset < openedStat.size) {
|
|
74
|
+
const bytesRead = readSync(fd, buffer, offset, openedStat.size - offset, offset);
|
|
75
|
+
if (bytesRead === 0) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
offset += bytesRead;
|
|
79
|
+
}
|
|
80
|
+
const finalStat = fstatSync(fd);
|
|
81
|
+
if (finalStat.dev !== openedStat.dev || finalStat.ino !== openedStat.ino) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return buffer.toString('utf8');
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
closeSync(fd);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
47
94
|
function isValidArtifactFile(rootPath, artifact) {
|
|
48
95
|
const artifactPath = resolve(rootPath, artifact);
|
|
49
96
|
try {
|
|
@@ -61,7 +108,7 @@ function isValidArtifactFile(rootPath, artifact) {
|
|
|
61
108
|
}
|
|
62
109
|
}
|
|
63
110
|
function waveManifestPath(changeId, index, wave) {
|
|
64
|
-
return buildArtifactRelativePath(changeId, 'architecture', 'waves', `wave-${index + 1}-${wave}.json`);
|
|
111
|
+
return buildArtifactRelativePath(changeId, 'rd', 'architecture', 'waves', `wave-${index + 1}-${wave}.json`);
|
|
65
112
|
}
|
|
66
113
|
function taskPurpose(taskId, goal) {
|
|
67
114
|
return `${taskId.replace(/^tech-/, '').replace(/-/g, ' ')} for ${goal}`;
|
|
@@ -82,7 +129,7 @@ function createTechGraph(request) {
|
|
|
82
129
|
: wave.name === 'review'
|
|
83
130
|
? [...documentTaskIds]
|
|
84
131
|
: [...reviewTaskIds];
|
|
85
|
-
const briefPath = buildArtifactRelativePath(request.changeId, 'architecture', 'workers', taskId, 'brief.md');
|
|
132
|
+
const briefPath = buildArtifactRelativePath(request.changeId, 'rd', 'architecture', 'workers', taskId, 'brief.md');
|
|
86
133
|
return {
|
|
87
134
|
taskId,
|
|
88
135
|
wave: wave.name,
|
|
@@ -104,10 +151,10 @@ function createTechGraph(request) {
|
|
|
104
151
|
waves,
|
|
105
152
|
tasks,
|
|
106
153
|
outputs: {
|
|
107
|
-
taskGraph: buildArtifactRelativePath(request.changeId, 'architecture', 'tech-task-graph.json'),
|
|
154
|
+
taskGraph: buildArtifactRelativePath(request.changeId, 'rd', 'architecture', 'tech-task-graph.json'),
|
|
108
155
|
waveManifests: waves.map((wave, index) => waveManifestPath(request.changeId, index, wave.name)),
|
|
109
|
-
reviewChecklist: buildArtifactRelativePath(request.changeId, 'architecture', 'tech-review-checklist.md'),
|
|
110
|
-
approvalTemplate: buildArtifactRelativePath(request.changeId, 'architecture', 'tech-approval-record.template.md'),
|
|
156
|
+
reviewChecklist: buildArtifactRelativePath(request.changeId, 'rd', 'architecture', 'tech-review-checklist.md'),
|
|
157
|
+
approvalTemplate: buildArtifactRelativePath(request.changeId, 'rd', 'architecture', 'tech-approval-record.template.md'),
|
|
111
158
|
},
|
|
112
159
|
blockedReasons: [],
|
|
113
160
|
nextActions: [],
|
|
@@ -156,8 +203,8 @@ export function getTechStatus(options) {
|
|
|
156
203
|
nextActions: [...WORKSPACE_UNAVAILABLE_NEXT_ACTIONS],
|
|
157
204
|
};
|
|
158
205
|
}
|
|
159
|
-
const rootPath = resolve(options.artifactWorkspacePath, '.peaks',
|
|
160
|
-
const approvalRecord = buildArtifactRelativePath(options.changeId, 'architecture', 'tech-approval-record.md');
|
|
206
|
+
const rootPath = resolve(options.artifactWorkspacePath, '.peaks', options.changeId, 'rd', 'architecture');
|
|
207
|
+
const approvalRecord = buildArtifactRelativePath(options.changeId, 'rd', 'architecture', 'tech-approval-record.md');
|
|
161
208
|
if (isEscapedArchitectureRoot(rootPath, options.artifactWorkspacePath)) {
|
|
162
209
|
return {
|
|
163
210
|
changeId: options.changeId,
|
|
@@ -195,11 +242,8 @@ export function getTechStatus(options) {
|
|
|
195
242
|
nextActions: ['Run peaks tech plan --dry-run, then persist and review the required tech artifacts.'],
|
|
196
243
|
};
|
|
197
244
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
approvalContent = readFileSync(join(rootPath, 'tech-approval-record.md'), 'utf8');
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
245
|
+
const approvalContent = readTechArtifactFile(rootPath, 'tech-approval-record.md');
|
|
246
|
+
if (approvalContent === null) {
|
|
203
247
|
return {
|
|
204
248
|
changeId: options.changeId,
|
|
205
249
|
status: 'blocked',
|
|
@@ -216,10 +216,10 @@ function createGoalCommand(goalPackage) {
|
|
|
216
216
|
function getResumeRequiredArtifacts(changeId) {
|
|
217
217
|
return [
|
|
218
218
|
buildArtifactRelativePath(changeId, 'prd', 'autonomous-goal-package.json'),
|
|
219
|
-
buildArtifactRelativePath(changeId, 'swarm', 'autonomous-rd-plan.json'),
|
|
220
|
-
buildArtifactRelativePath(changeId, 'swarm', 'checkpoints', 'checkpoint-1.json'),
|
|
221
|
-
buildArtifactRelativePath(changeId, 'swarm', 'evidence', 'validation-report.md'),
|
|
222
|
-
buildArtifactRelativePath(changeId, 'swarm', 'resume-instructions.md')
|
|
219
|
+
buildArtifactRelativePath(changeId, 'rd', 'swarm', 'autonomous-rd-plan.json'),
|
|
220
|
+
buildArtifactRelativePath(changeId, 'rd', 'swarm', 'checkpoints', 'checkpoint-1.json'),
|
|
221
|
+
buildArtifactRelativePath(changeId, 'rd', 'swarm', 'evidence', 'validation-report.md'),
|
|
222
|
+
buildArtifactRelativePath(changeId, 'rd', 'swarm', 'resume-instructions.md')
|
|
223
223
|
];
|
|
224
224
|
}
|
|
225
225
|
function isObjectRecord(value) {
|
|
@@ -249,8 +249,31 @@ function readResumeArtifact(artifactWorkspacePath, artifact) {
|
|
|
249
249
|
if (artifactStat.isSymbolicLink() || !artifactStat.isFile() || artifactStat.size > MAX_RESUME_ARTIFACT_BYTES) {
|
|
250
250
|
return null;
|
|
251
251
|
}
|
|
252
|
+
const pathSegments = artifact.replace(/\\/g, '/').split('/');
|
|
253
|
+
if (pathSegments.length < 4 || pathSegments[0] !== '.peaks') {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const sessionRootPath = resolve(artifactWorkspacePath, '.peaks', pathSegments[1]);
|
|
257
|
+
const roleRootPath = resolve(sessionRootPath, pathSegments[2]);
|
|
258
|
+
if (lstatSync(sessionRootPath).isSymbolicLink() || lstatSync(roleRootPath).isSymbolicLink()) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
let allowedRootRealPath;
|
|
262
|
+
if (pathSegments[2] === 'rd') {
|
|
263
|
+
const swarmRootPath = resolve(roleRootPath, 'swarm');
|
|
264
|
+
if (pathSegments[3] !== 'swarm' || lstatSync(swarmRootPath).isSymbolicLink()) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
allowedRootRealPath = realpathSync(swarmRootPath);
|
|
268
|
+
}
|
|
269
|
+
else if (pathSegments[2] === 'prd') {
|
|
270
|
+
allowedRootRealPath = realpathSync(roleRootPath);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
252
275
|
const artifactRealPath = realpathSync(artifactPath);
|
|
253
|
-
if (!isInsidePath(
|
|
276
|
+
if (!isInsidePath(allowedRootRealPath, artifactWorkspaceRealPath) || !isInsidePath(artifactRealPath, allowedRootRealPath)) {
|
|
254
277
|
return null;
|
|
255
278
|
}
|
|
256
279
|
const fd = openSync(artifactPath, 'r');
|
|
@@ -385,7 +408,7 @@ function isSafeEvidenceRef(ref) {
|
|
|
385
408
|
return ref.toLowerCase() !== 'validation-report.md' && /^[A-Za-z0-9][A-Za-z0-9._-]*\.md$/.test(ref) && !ref.includes('..');
|
|
386
409
|
}
|
|
387
410
|
function evidenceRefsExist(artifactWorkspacePath, changeId, refs) {
|
|
388
|
-
return refs.every((ref) => isSafeEvidenceRef(ref) && readResumeArtifact(artifactWorkspacePath, buildArtifactRelativePath(changeId, 'swarm', 'evidence', ref)) !== null);
|
|
411
|
+
return refs.every((ref) => isSafeEvidenceRef(ref) && readResumeArtifact(artifactWorkspacePath, buildArtifactRelativePath(changeId, 'rd', 'swarm', 'evidence', ref)) !== null);
|
|
389
412
|
}
|
|
390
413
|
function hasMatchingEvidenceRefs(artifactWorkspacePath, changeId, validationReportContent, checkpointContent) {
|
|
391
414
|
const expectedRefs = getCheckpointValidationRefs(checkpointContent);
|
|
@@ -425,8 +448,8 @@ function getResumeArtifactsStatus(artifactWorkspacePath, requiredArtifacts, chan
|
|
|
425
448
|
hasInvalidArtifact = true;
|
|
426
449
|
}
|
|
427
450
|
}
|
|
428
|
-
const checkpointContent = artifactContents.get(buildArtifactRelativePath(changeId, 'swarm', 'checkpoints', 'checkpoint-1.json'));
|
|
429
|
-
const validationReportContent = artifactContents.get(buildArtifactRelativePath(changeId, 'swarm', 'evidence', 'validation-report.md'));
|
|
451
|
+
const checkpointContent = artifactContents.get(buildArtifactRelativePath(changeId, 'rd', 'swarm', 'checkpoints', 'checkpoint-1.json'));
|
|
452
|
+
const validationReportContent = artifactContents.get(buildArtifactRelativePath(changeId, 'rd', 'swarm', 'evidence', 'validation-report.md'));
|
|
430
453
|
if (!checkpointContent || !validationReportContent || !hasMatchingEvidenceRefs(artifactWorkspacePath, changeId, validationReportContent, checkpointContent)) {
|
|
431
454
|
hasInvalidArtifact = true;
|
|
432
455
|
}
|
|
@@ -61,7 +61,7 @@ export function isUnsafeArtifactPath(path) {
|
|
|
61
61
|
export function buildArtifactRelativePath(changeId, ...segments) {
|
|
62
62
|
validateChangeIdOrThrow(changeId);
|
|
63
63
|
const joined = segments.map((segment) => normalizeForwardSlashes(segment)).join('/');
|
|
64
|
-
const candidatePath = `.peaks
|
|
64
|
+
const candidatePath = `.peaks/${changeId}/${joined}`;
|
|
65
65
|
if (isUnsafeArtifactPath(joined) || isUnsafeArtifactPath(candidatePath)) {
|
|
66
66
|
throw new ChangeIdValidationError(changeId);
|
|
67
67
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "1.0.
|
|
1
|
+
export declare const CLI_VERSION = "1.0.4";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.0.
|
|
1
|
+
export const CLI_VERSION = "1.0.4";
|