peaks-cli 1.0.0
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/LICENSE +52 -0
- package/README.md +417 -0
- package/bin/peaks.js +2 -0
- package/dist/src/cli/cli-helpers.d.ts +25 -0
- package/dist/src/cli/cli-helpers.js +78 -0
- package/dist/src/cli/commands/capability-commands.d.ts +5 -0
- package/dist/src/cli/commands/capability-commands.js +46 -0
- package/dist/src/cli/commands/capability-worker-config-sc-commands.d.ts +3 -0
- package/dist/src/cli/commands/capability-worker-config-sc-commands.js +10 -0
- package/dist/src/cli/commands/config-commands.d.ts +3 -0
- package/dist/src/cli/commands/config-commands.js +212 -0
- package/dist/src/cli/commands/core-artifact-commands.d.ts +3 -0
- package/dist/src/cli/commands/core-artifact-commands.js +200 -0
- package/dist/src/cli/commands/sc-commands.d.ts +3 -0
- package/dist/src/cli/commands/sc-commands.js +37 -0
- package/dist/src/cli/commands/worker-commands.d.ts +3 -0
- package/dist/src/cli/commands/worker-commands.js +52 -0
- package/dist/src/cli/commands/workflow-commands.d.ts +3 -0
- package/dist/src/cli/commands/workflow-commands.js +257 -0
- package/dist/src/cli/index.d.ts +1 -0
- package/dist/src/cli/index.js +14 -0
- package/dist/src/cli/program.d.ts +4 -0
- package/dist/src/cli/program.js +13 -0
- package/dist/src/services/artifacts/artifact-service.d.ts +43 -0
- package/dist/src/services/artifacts/artifact-service.js +97 -0
- package/dist/src/services/artifacts/workspace-service.d.ts +33 -0
- package/dist/src/services/artifacts/workspace-service.js +254 -0
- package/dist/src/services/config/config-service.d.ts +29 -0
- package/dist/src/services/config/config-service.js +501 -0
- package/dist/src/services/config/config-types.d.ts +63 -0
- package/dist/src/services/config/config-types.js +16 -0
- package/dist/src/services/config/model-routing.d.ts +4 -0
- package/dist/src/services/config/model-routing.js +15 -0
- package/dist/src/services/doctor/doctor-service.d.ts +18 -0
- package/dist/src/services/doctor/doctor-service.js +68 -0
- package/dist/src/services/memory/project-memory-service.d.ts +79 -0
- package/dist/src/services/memory/project-memory-service.js +306 -0
- package/dist/src/services/profiles/profile-service.d.ts +6 -0
- package/dist/src/services/profiles/profile-service.js +19 -0
- package/dist/src/services/providers/minimax-provider-service.d.ts +24 -0
- package/dist/src/services/providers/minimax-provider-service.js +143 -0
- package/dist/src/services/providers/minimax-worker-service.d.ts +21 -0
- package/dist/src/services/providers/minimax-worker-service.js +80 -0
- package/dist/src/services/proxy/proxy-service.d.ts +7 -0
- package/dist/src/services/proxy/proxy-service.js +31 -0
- package/dist/src/services/rd/rd-service.d.ts +88 -0
- package/dist/src/services/rd/rd-service.js +370 -0
- package/dist/src/services/recommendations/capability-availability.d.ts +5 -0
- package/dist/src/services/recommendations/capability-availability.js +40 -0
- package/dist/src/services/recommendations/capability-map-service.d.ts +7 -0
- package/dist/src/services/recommendations/capability-map-service.js +131 -0
- package/dist/src/services/recommendations/capability-seed-items.d.ts +2 -0
- package/dist/src/services/recommendations/capability-seed-items.js +131 -0
- package/dist/src/services/recommendations/capability-seed-mappings.d.ts +2 -0
- package/dist/src/services/recommendations/capability-seed-mappings.js +42 -0
- package/dist/src/services/recommendations/capability-seed-sources.d.ts +2 -0
- package/dist/src/services/recommendations/capability-seed-sources.js +35 -0
- package/dist/src/services/recommendations/recommendation-service.d.ts +8 -0
- package/dist/src/services/recommendations/recommendation-service.js +106 -0
- package/dist/src/services/recommendations/recommendation-types.d.ts +129 -0
- package/dist/src/services/recommendations/recommendation-types.js +1 -0
- package/dist/src/services/recommendations/seed-capability-catalog.d.ts +3 -0
- package/dist/src/services/recommendations/seed-capability-catalog.js +3 -0
- package/dist/src/services/refactor/refactor-service.d.ts +9 -0
- package/dist/src/services/refactor/refactor-service.js +33 -0
- package/dist/src/services/sc/index.d.ts +1 -0
- package/dist/src/services/sc/index.js +1 -0
- package/dist/src/services/sc/sc-service.d.ts +79 -0
- package/dist/src/services/sc/sc-service.js +223 -0
- package/dist/src/services/skills/skill-registry.d.ts +17 -0
- package/dist/src/services/skills/skill-registry.js +40 -0
- package/dist/src/services/standards/project-standards-service.d.ts +82 -0
- package/dist/src/services/standards/project-standards-service.js +383 -0
- package/dist/src/services/tech/tech-service.d.ts +69 -0
- package/dist/src/services/tech/tech-service.js +236 -0
- package/dist/src/services/workflow/workflow-autonomous-service.d.ts +99 -0
- package/dist/src/services/workflow/workflow-autonomous-service.js +526 -0
- package/dist/src/services/workflow/workflow-router-service.d.ts +85 -0
- package/dist/src/services/workflow/workflow-router-service.js +213 -0
- package/dist/src/shared/change-id.d.ts +15 -0
- package/dist/src/shared/change-id.js +76 -0
- package/dist/src/shared/frontmatter.d.ts +6 -0
- package/dist/src/shared/frontmatter.js +47 -0
- package/dist/src/shared/fs-utils.d.ts +4 -0
- package/dist/src/shared/fs-utils.js +16 -0
- package/dist/src/shared/fs.d.ts +4 -0
- package/dist/src/shared/fs.js +26 -0
- package/dist/src/shared/path-utils.d.ts +13 -0
- package/dist/src/shared/path-utils.js +56 -0
- package/dist/src/shared/paths.d.ts +6 -0
- package/dist/src/shared/paths.js +40 -0
- package/dist/src/shared/planner-response.d.ts +21 -0
- package/dist/src/shared/planner-response.js +26 -0
- package/dist/src/shared/platform.d.ts +6 -0
- package/dist/src/shared/platform.js +11 -0
- package/dist/src/shared/process.d.ts +5 -0
- package/dist/src/shared/process.js +12 -0
- package/dist/src/shared/result.d.ts +13 -0
- package/dist/src/shared/result.js +32 -0
- package/package.json +49 -0
- package/schemas/approval-record.schema.json +14 -0
- package/schemas/artifact-manifest.schema.json +16 -0
- package/schemas/artifact-retention-report.schema.json +17 -0
- package/schemas/artifact-workspace.schema.json +22 -0
- package/schemas/capability-availability.schema.json +36 -0
- package/schemas/capability-item.schema.json +37 -0
- package/schemas/capability-source.schema.json +30 -0
- package/schemas/change-impact.schema.json +15 -0
- package/schemas/context-capsule.schema.json +16 -0
- package/schemas/recommendation-plan.schema.json +37 -0
- package/schemas/refactor-slice-spec.schema.json +19 -0
- package/scripts/clean-dist.mjs +8 -0
- package/scripts/install-skills.mjs +76 -0
- package/scripts/watch.mjs +389 -0
- package/skills/peaks-prd/SKILL.md +42 -0
- package/skills/peaks-prd/references/artifact-contracts.md +3 -0
- package/skills/peaks-prd/references/command-migration.md +3 -0
- package/skills/peaks-prd/references/workflow.md +11 -0
- package/skills/peaks-qa/SKILL.md +45 -0
- package/skills/peaks-qa/references/artifact-contracts.md +3 -0
- package/skills/peaks-qa/references/command-migration.md +3 -0
- package/skills/peaks-qa/references/regression-gates.md +16 -0
- package/skills/peaks-rd/SKILL.md +56 -0
- package/skills/peaks-rd/references/artifact-contracts.md +3 -0
- package/skills/peaks-rd/references/command-migration.md +3 -0
- package/skills/peaks-rd/references/refactor-workflow.md +31 -0
- package/skills/peaks-sc/SKILL.md +30 -0
- package/skills/peaks-sc/references/artifact-contracts.md +3 -0
- package/skills/peaks-sc/references/artifact-retention.md +14 -0
- package/skills/peaks-sc/references/command-migration.md +3 -0
- package/skills/peaks-solo/SKILL.md +63 -0
- package/skills/peaks-solo/references/artifact-contracts.md +3 -0
- package/skills/peaks-solo/references/command-migration.md +3 -0
- package/skills/peaks-solo/references/refactor-mode.md +22 -0
- package/skills/peaks-solo/references/workflow.md +14 -0
- package/skills/peaks-txt/SKILL.md +48 -0
- package/skills/peaks-txt/references/artifact-contracts.md +3 -0
- package/skills/peaks-txt/references/command-migration.md +3 -0
- package/skills/peaks-txt/references/context-capsule.md +20 -0
- package/skills/peaks-ui/SKILL.md +35 -0
- package/skills/peaks-ui/references/artifact-contracts.md +3 -0
- package/skills/peaks-ui/references/command-migration.md +3 -0
- package/skills/peaks-ui/references/workflow.md +11 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, realpathSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
+
const SOURCE = {
|
|
4
|
+
sourceId: 'everything-claude-code',
|
|
5
|
+
url: 'https://github.com/affaan-m/everything-claude-code',
|
|
6
|
+
usage: 'curated-baseline-reference'
|
|
7
|
+
};
|
|
8
|
+
const SKILL_PREFLIGHT = {
|
|
9
|
+
appliesTo: ['peaks-rd', 'peaks-qa', 'peaks-solo'],
|
|
10
|
+
summary: 'peaks-rd、peaks-qa、peaks-solo 进入代码仓工作流时自动 preflight 项目规范。'
|
|
11
|
+
};
|
|
12
|
+
const SUPPORTED_LANGUAGES = new Set(['generic', 'typescript', 'javascript', 'python', 'go', 'rust']);
|
|
13
|
+
function normalizeRoot(path) {
|
|
14
|
+
return realpathSync(resolve(path));
|
|
15
|
+
}
|
|
16
|
+
function isInsidePath(childPath, parentPath) {
|
|
17
|
+
const rel = relative(parentPath, childPath);
|
|
18
|
+
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
19
|
+
}
|
|
20
|
+
function assertDirectoryNotSymlink(path) {
|
|
21
|
+
if (existsSync(path) && lstatSync(path).isSymbolicLink()) {
|
|
22
|
+
throw new Error('Project standards directory must stay inside the project root');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function assertRealPathInsideProject(path, projectRoot) {
|
|
26
|
+
if (!isInsidePath(realpathSync(path), projectRoot)) {
|
|
27
|
+
throw new Error('Project standards write target must stay inside the project root');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function assertSafeClaudeMdPath(filePath, projectRoot) {
|
|
31
|
+
if (!existsSync(filePath))
|
|
32
|
+
return;
|
|
33
|
+
if (lstatSync(filePath).isSymbolicLink() || !isInsidePath(realpathSync(filePath), projectRoot)) {
|
|
34
|
+
throw new Error('Project standards CLAUDE.md must stay inside the project root');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function assertSafeStandardsRoot(projectRoot) {
|
|
38
|
+
const resolvedRoot = normalizeRoot(projectRoot);
|
|
39
|
+
const claudeDir = join(resolvedRoot, '.claude');
|
|
40
|
+
const rulesDir = join(claudeDir, 'rules');
|
|
41
|
+
assertDirectoryNotSymlink(claudeDir);
|
|
42
|
+
assertDirectoryNotSymlink(rulesDir);
|
|
43
|
+
if (existsSync(rulesDir)) {
|
|
44
|
+
assertRealPathInsideProject(rulesDir, resolvedRoot);
|
|
45
|
+
return realpathSync(rulesDir);
|
|
46
|
+
}
|
|
47
|
+
return rulesDir;
|
|
48
|
+
}
|
|
49
|
+
function parseLanguage(value) {
|
|
50
|
+
const normalized = value.trim().toLowerCase();
|
|
51
|
+
if (SUPPORTED_LANGUAGES.has(normalized)) {
|
|
52
|
+
return normalized;
|
|
53
|
+
}
|
|
54
|
+
throw new Error('Unsupported standards language');
|
|
55
|
+
}
|
|
56
|
+
function detectLanguage(projectRoot) {
|
|
57
|
+
if (existsSync(join(projectRoot, 'tsconfig.json')))
|
|
58
|
+
return 'typescript';
|
|
59
|
+
if (existsSync(join(projectRoot, 'package.json')))
|
|
60
|
+
return 'javascript';
|
|
61
|
+
if (existsSync(join(projectRoot, 'pyproject.toml')) || existsSync(join(projectRoot, 'requirements.txt')))
|
|
62
|
+
return 'python';
|
|
63
|
+
if (existsSync(join(projectRoot, 'go.mod')))
|
|
64
|
+
return 'go';
|
|
65
|
+
if (existsSync(join(projectRoot, 'Cargo.toml')))
|
|
66
|
+
return 'rust';
|
|
67
|
+
return 'generic';
|
|
68
|
+
}
|
|
69
|
+
function renderHeader(title) {
|
|
70
|
+
return [
|
|
71
|
+
`# ${title}`,
|
|
72
|
+
'',
|
|
73
|
+
'Source: Peaks curated baseline; everything-claude-code reference: https://github.com/affaan-m/everything-claude-code',
|
|
74
|
+
'Scope: project-local standards for peaks-rd, peaks-qa, and peaks-solo workflow preflight.',
|
|
75
|
+
''
|
|
76
|
+
].join('\n');
|
|
77
|
+
}
|
|
78
|
+
function renderClaudeMd(language) {
|
|
79
|
+
return [
|
|
80
|
+
'# Project Instructions',
|
|
81
|
+
'',
|
|
82
|
+
'> 🤖 AI 生成,请审阅',
|
|
83
|
+
'',
|
|
84
|
+
'This repository uses project-local Peaks standards. Existing repository conventions override generic generated guidance.',
|
|
85
|
+
'',
|
|
86
|
+
'Peaks workflow automation:',
|
|
87
|
+
'- peaks-rd checks these standards before RD planning or implementation work.',
|
|
88
|
+
'- peaks-qa checks code review and security guidance before verification work.',
|
|
89
|
+
'- peaks-solo summarizes RD and QA standards preflight before end-to-end code workflows.',
|
|
90
|
+
'',
|
|
91
|
+
'Rules:',
|
|
92
|
+
'- Read `.claude/rules/common/coding-style.md` before editing code.',
|
|
93
|
+
'- Read `.claude/rules/common/code-review.md` before reviewing changes.',
|
|
94
|
+
'- Read `.claude/rules/common/security.md` before touching filesystem, user input, external calls, auth, or secrets.',
|
|
95
|
+
`- Read .claude/rules/${language}/coding-style.md for language-specific standards when applicable.`,
|
|
96
|
+
'',
|
|
97
|
+
'External reference: https://github.com/affaan-m/everything-claude-code is used as a curated reference only. Do not execute or install external content without explicit approval.',
|
|
98
|
+
''
|
|
99
|
+
].join('\n');
|
|
100
|
+
}
|
|
101
|
+
function renderCommonCodingStyle() {
|
|
102
|
+
return `${renderHeader('Common Coding Standards')}- Prefer simple, readable code over clever abstractions.
|
|
103
|
+
- Keep functions focused and files cohesive.
|
|
104
|
+
- Use immutable updates unless a language-specific convention explicitly favors mutation.
|
|
105
|
+
- Validate user input, external data, file paths, and configuration at system boundaries.
|
|
106
|
+
- Preserve existing project conventions when they are stricter than this baseline.
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
function renderCodeReview() {
|
|
110
|
+
return `${renderHeader('Code Review Standards')}- Review diffs for correctness, maintainability, test coverage, and regression risk.
|
|
111
|
+
- Treat missing tests for changed behavior as a blocker unless the change is documentation-only.
|
|
112
|
+
- Verify code paths that handle filesystem, external APIs, credentials, user input, or generated artifacts.
|
|
113
|
+
- peaks-qa must use this guidance as part of code workflow preflight and final verification.
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
function renderSecurity() {
|
|
117
|
+
return `${renderHeader('Security Review Standards')}- Never hardcode secrets, API keys, passwords, tokens, or credentials.
|
|
118
|
+
- Do not send private code or secrets to external services without explicit user authorization.
|
|
119
|
+
- Guard filesystem writes against path traversal, symlink, and junction escapes.
|
|
120
|
+
- Require explicit confirmation for destructive actions, external state changes, and credential use.
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
function renderLanguageCodingStyle(language) {
|
|
124
|
+
const languageName = language === 'generic' ? 'Generic' : language[0].toUpperCase() + language.slice(1);
|
|
125
|
+
const typeSafetyRule = language === 'typescript' || language === 'javascript'
|
|
126
|
+
? '- Do not add new `any` types; use explicit domain types, generics, or `unknown` with narrowing.\n'
|
|
127
|
+
: '';
|
|
128
|
+
return `${renderHeader(`${languageName} Coding Standards`)}- Apply project-local conventions before generic ${language} guidance.
|
|
129
|
+
- Keep public APIs typed or documented according to ${language} ecosystem norms.
|
|
130
|
+
${typeSafetyRule}- Prefer standard tooling and existing project scripts for formatting, linting, tests, and coverage.
|
|
131
|
+
- peaks-rd must check this file before planning code changes in ${language} projects.
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
134
|
+
function renderManagedClaudeMdIndex(language) {
|
|
135
|
+
return [
|
|
136
|
+
'<!-- peaks-standards:index:start -->',
|
|
137
|
+
'## Peaks Standards Index',
|
|
138
|
+
'- Constitution: `CLAUDE.md` is the repository-wide constitution.',
|
|
139
|
+
'- Local laws: `.claude/rules/**` are project-local laws and are created only when missing.',
|
|
140
|
+
'- Managed by: `peaks standards update`.',
|
|
141
|
+
'- Managed files:',
|
|
142
|
+
' - `.claude/rules/common/code-review.md`',
|
|
143
|
+
' - `.claude/rules/common/coding-style.md`',
|
|
144
|
+
' - `.claude/rules/common/security.md`',
|
|
145
|
+
` - .claude/rules/${language}/coding-style.md`,
|
|
146
|
+
'- Conflict note: keep the existing body unchanged and resolve any disagreement manually before the next standards update.',
|
|
147
|
+
'<!-- peaks-standards:index:end -->',
|
|
148
|
+
''
|
|
149
|
+
].join('\n');
|
|
150
|
+
}
|
|
151
|
+
function readFileIfExists(path) {
|
|
152
|
+
if (!existsSync(path))
|
|
153
|
+
return null;
|
|
154
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
155
|
+
try {
|
|
156
|
+
return readFileSync(fd, 'utf8');
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
closeSync(fd);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function writeMissingStandardsRules(plan) {
|
|
163
|
+
const writtenFiles = [];
|
|
164
|
+
for (const write of plan.plannedWrites) {
|
|
165
|
+
if (write.relativePath === 'CLAUDE.md' || write.status === 'existing')
|
|
166
|
+
continue;
|
|
167
|
+
const targetPath = resolve(write.filePath);
|
|
168
|
+
const targetDir = dirname(targetPath);
|
|
169
|
+
mkdirSync(targetDir, { recursive: true });
|
|
170
|
+
assertRealPathInsideProject(targetDir, plan.projectRoot);
|
|
171
|
+
writeNewFile(targetPath, write.content);
|
|
172
|
+
writtenFiles.push(write.relativePath);
|
|
173
|
+
}
|
|
174
|
+
return writtenFiles;
|
|
175
|
+
}
|
|
176
|
+
function createTemplates(language) {
|
|
177
|
+
return [
|
|
178
|
+
{ relativePath: 'CLAUDE.md', content: renderClaudeMd(language) },
|
|
179
|
+
{ relativePath: '.claude/rules/common/code-review.md', content: renderCodeReview() },
|
|
180
|
+
{ relativePath: '.claude/rules/common/coding-style.md', content: renderCommonCodingStyle() },
|
|
181
|
+
{ relativePath: '.claude/rules/common/security.md', content: renderSecurity() },
|
|
182
|
+
{ relativePath: `.claude/rules/${language}/coding-style.md`, content: renderLanguageCodingStyle(language) }
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
function createManagedClaudeBlock(language) {
|
|
186
|
+
return renderManagedClaudeMdIndex(language);
|
|
187
|
+
}
|
|
188
|
+
function buildClaudeUpdate(projectRoot, language) {
|
|
189
|
+
const filePath = resolve(projectRoot, 'CLAUDE.md');
|
|
190
|
+
assertSafeClaudeMdPath(filePath, projectRoot);
|
|
191
|
+
const existingContent = readFileIfExists(filePath);
|
|
192
|
+
const managedBlock = createManagedClaudeBlock(language);
|
|
193
|
+
if (existingContent === null) {
|
|
194
|
+
return {
|
|
195
|
+
relativePath: 'CLAUDE.md',
|
|
196
|
+
filePath,
|
|
197
|
+
status: 'planned',
|
|
198
|
+
content: `${renderClaudeMd(language).trimEnd()}\n\n${managedBlock}`,
|
|
199
|
+
appendBlock: '',
|
|
200
|
+
reviewSuggestions: []
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const existingBlockStart = existingContent.indexOf('<!-- peaks-standards:index:start -->');
|
|
204
|
+
if (existingBlockStart < 0) {
|
|
205
|
+
return {
|
|
206
|
+
relativePath: 'CLAUDE.md',
|
|
207
|
+
filePath,
|
|
208
|
+
status: 'appended',
|
|
209
|
+
content: `${existingContent.trimEnd()}
|
|
210
|
+
|
|
211
|
+
${managedBlock}`,
|
|
212
|
+
appendBlock: `
|
|
213
|
+
|
|
214
|
+
${managedBlock}`,
|
|
215
|
+
reviewSuggestions: []
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const existingManagedBlock = existingContent.slice(existingBlockStart).trimEnd();
|
|
219
|
+
if (existingManagedBlock === managedBlock.trimEnd()) {
|
|
220
|
+
return {
|
|
221
|
+
relativePath: 'CLAUDE.md',
|
|
222
|
+
filePath,
|
|
223
|
+
status: 'existing',
|
|
224
|
+
content: existingContent,
|
|
225
|
+
appendBlock: '',
|
|
226
|
+
reviewSuggestions: []
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
relativePath: 'CLAUDE.md',
|
|
231
|
+
filePath,
|
|
232
|
+
status: 'review',
|
|
233
|
+
content: existingContent,
|
|
234
|
+
appendBlock: '',
|
|
235
|
+
reviewSuggestions: ['Existing CLAUDE.md already has a managed standards block. Review the managed block manually before changing it.']
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function buildWrite(projectRoot, template) {
|
|
239
|
+
const filePath = resolve(projectRoot, template.relativePath);
|
|
240
|
+
return {
|
|
241
|
+
...template,
|
|
242
|
+
filePath,
|
|
243
|
+
status: existsSync(filePath) ? 'existing' : 'planned'
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function writeNewFile(path, content) {
|
|
247
|
+
const fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
248
|
+
try {
|
|
249
|
+
writeFileSync(fd, content, 'utf8');
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
closeSync(fd);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function appendExistingFile(path, content) {
|
|
256
|
+
const fd = openSync(path, constants.O_WRONLY | constants.O_APPEND | constants.O_NOFOLLOW);
|
|
257
|
+
try {
|
|
258
|
+
writeFileSync(fd, content, 'utf8');
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
closeSync(fd);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
export function createProjectStandardsInitPlan(options) {
|
|
265
|
+
const projectRoot = normalizeRoot(options.projectRoot);
|
|
266
|
+
assertSafeStandardsRoot(projectRoot);
|
|
267
|
+
const language = options.language === undefined ? detectLanguage(projectRoot) : parseLanguage(options.language);
|
|
268
|
+
const plannedWrites = createTemplates(language).map((template) => buildWrite(projectRoot, template));
|
|
269
|
+
return {
|
|
270
|
+
apply: options.apply ?? false,
|
|
271
|
+
projectRoot,
|
|
272
|
+
language,
|
|
273
|
+
source: SOURCE,
|
|
274
|
+
skillPreflight: SKILL_PREFLIGHT,
|
|
275
|
+
plannedWrites
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
export function createProjectStandardsUpdatePlan(options) {
|
|
279
|
+
const basePlan = createProjectStandardsInitPlan(options);
|
|
280
|
+
const claudeMd = buildClaudeUpdate(basePlan.projectRoot, basePlan.language);
|
|
281
|
+
return {
|
|
282
|
+
...basePlan,
|
|
283
|
+
claudeMd
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
export function executeProjectStandardsInit(options) {
|
|
287
|
+
const plan = createProjectStandardsInitPlan(options);
|
|
288
|
+
const writtenFiles = [];
|
|
289
|
+
if (plan.apply) {
|
|
290
|
+
assertSafeStandardsRoot(plan.projectRoot);
|
|
291
|
+
for (const write of plan.plannedWrites) {
|
|
292
|
+
if (write.status === 'existing')
|
|
293
|
+
continue;
|
|
294
|
+
const targetPath = resolve(write.filePath);
|
|
295
|
+
const targetDir = dirname(targetPath);
|
|
296
|
+
mkdirSync(targetDir, { recursive: true });
|
|
297
|
+
assertRealPathInsideProject(targetDir, plan.projectRoot);
|
|
298
|
+
if (write.relativePath === 'CLAUDE.md') {
|
|
299
|
+
assertSafeClaudeMdPath(targetPath, plan.projectRoot);
|
|
300
|
+
}
|
|
301
|
+
writeNewFile(targetPath, write.content);
|
|
302
|
+
writtenFiles.push(write.relativePath);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
...plan,
|
|
307
|
+
plannedWrites: plan.plannedWrites.map((write) => writtenFiles.includes(write.relativePath) ? { ...write, status: 'written' } : write),
|
|
308
|
+
writtenFiles
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
export function executeProjectStandardsUpdate(options) {
|
|
312
|
+
const plan = createProjectStandardsUpdatePlan(options);
|
|
313
|
+
const writtenFiles = [];
|
|
314
|
+
const appendedFiles = [];
|
|
315
|
+
const reviewSuggestions = [...plan.claudeMd.reviewSuggestions];
|
|
316
|
+
let claudeMd = { ...plan.claudeMd };
|
|
317
|
+
if (plan.apply) {
|
|
318
|
+
assertSafeStandardsRoot(plan.projectRoot);
|
|
319
|
+
writtenFiles.push(...writeMissingStandardsRules(plan));
|
|
320
|
+
const targetPath = resolve(claudeMd.filePath);
|
|
321
|
+
const targetDir = dirname(targetPath);
|
|
322
|
+
mkdirSync(targetDir, { recursive: true });
|
|
323
|
+
assertRealPathInsideProject(targetDir, plan.projectRoot);
|
|
324
|
+
assertSafeClaudeMdPath(targetPath, plan.projectRoot);
|
|
325
|
+
if (claudeMd.status === 'planned') {
|
|
326
|
+
writeNewFile(targetPath, claudeMd.content);
|
|
327
|
+
writtenFiles.push(claudeMd.relativePath);
|
|
328
|
+
claudeMd = { ...claudeMd, status: 'written' };
|
|
329
|
+
}
|
|
330
|
+
else if (claudeMd.status === 'appended') {
|
|
331
|
+
appendExistingFile(targetPath, claudeMd.appendBlock);
|
|
332
|
+
appendedFiles.push(claudeMd.relativePath);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const plannedWrites = plan.plannedWrites.map((write) => {
|
|
336
|
+
if (write.relativePath === 'CLAUDE.md') {
|
|
337
|
+
return { ...write, status: claudeMd.status };
|
|
338
|
+
}
|
|
339
|
+
if (writtenFiles.includes(write.relativePath)) {
|
|
340
|
+
return { ...write, status: 'written' };
|
|
341
|
+
}
|
|
342
|
+
return write;
|
|
343
|
+
});
|
|
344
|
+
return {
|
|
345
|
+
...plan,
|
|
346
|
+
claudeMd,
|
|
347
|
+
plannedWrites,
|
|
348
|
+
writtenFiles,
|
|
349
|
+
appendedFiles,
|
|
350
|
+
reviewSuggestions
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
export function summarizeProjectStandardsInitResult(result) {
|
|
354
|
+
return {
|
|
355
|
+
apply: result.apply,
|
|
356
|
+
projectRoot: result.projectRoot,
|
|
357
|
+
language: result.language,
|
|
358
|
+
source: result.source,
|
|
359
|
+
skillPreflight: result.skillPreflight,
|
|
360
|
+
plannedWrites: result.plannedWrites.map((write) => ({ relativePath: write.relativePath, status: write.status })),
|
|
361
|
+
writtenFiles: result.writtenFiles,
|
|
362
|
+
skippedFiles: result.plannedWrites.filter((write) => write.status === 'existing').map((write) => write.relativePath)
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
export function summarizeProjectStandardsUpdateResult(result) {
|
|
366
|
+
return {
|
|
367
|
+
apply: result.apply,
|
|
368
|
+
projectRoot: result.projectRoot,
|
|
369
|
+
language: result.language,
|
|
370
|
+
source: result.source,
|
|
371
|
+
skillPreflight: result.skillPreflight,
|
|
372
|
+
plannedWrites: result.plannedWrites.map((write) => ({ relativePath: write.relativePath, status: write.status })),
|
|
373
|
+
writtenFiles: result.writtenFiles,
|
|
374
|
+
appendedFiles: result.appendedFiles,
|
|
375
|
+
skippedFiles: result.plannedWrites.filter((write) => write.status === 'existing').map((write) => write.relativePath),
|
|
376
|
+
reviewSuggestions: result.reviewSuggestions,
|
|
377
|
+
claudeMd: {
|
|
378
|
+
relativePath: result.claudeMd.relativePath,
|
|
379
|
+
status: result.claudeMd.status,
|
|
380
|
+
reviewSuggestions: result.claudeMd.reviewSuggestions
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { WorkspaceConfig } from '../config/config-types.js';
|
|
2
|
+
export type TechWaveName = 'scan' | 'document' | 'review' | 'reducer';
|
|
3
|
+
export type TechStatusValue = 'unavailable' | 'missing' | 'blocked' | 'approved';
|
|
4
|
+
export type TechPlanRequest = {
|
|
5
|
+
changeId: string;
|
|
6
|
+
goal: string;
|
|
7
|
+
swarm: boolean;
|
|
8
|
+
dryRun: true;
|
|
9
|
+
artifactWorkspacePath?: string;
|
|
10
|
+
workspace?: WorkspaceConfig;
|
|
11
|
+
};
|
|
12
|
+
export type TechWave = {
|
|
13
|
+
name: TechWaveName;
|
|
14
|
+
taskIds: string[];
|
|
15
|
+
};
|
|
16
|
+
export type TechTask = {
|
|
17
|
+
taskId: string;
|
|
18
|
+
wave: TechWaveName;
|
|
19
|
+
workerKind: string;
|
|
20
|
+
purpose: string;
|
|
21
|
+
inputs: string[];
|
|
22
|
+
outputs: string[];
|
|
23
|
+
dependsOn: string[];
|
|
24
|
+
conflictGroup: string;
|
|
25
|
+
briefPath: string;
|
|
26
|
+
};
|
|
27
|
+
export type TechPlanGraph = {
|
|
28
|
+
available: true;
|
|
29
|
+
changeId: string;
|
|
30
|
+
goal: string;
|
|
31
|
+
swarm: boolean;
|
|
32
|
+
dryRun: true;
|
|
33
|
+
artifactRoot: string;
|
|
34
|
+
waves: TechWave[];
|
|
35
|
+
tasks: TechTask[];
|
|
36
|
+
outputs: {
|
|
37
|
+
taskGraph: string;
|
|
38
|
+
waveManifests: string[];
|
|
39
|
+
reviewChecklist: string;
|
|
40
|
+
approvalTemplate: string;
|
|
41
|
+
};
|
|
42
|
+
blockedReasons: string[];
|
|
43
|
+
nextActions: string[];
|
|
44
|
+
};
|
|
45
|
+
export type TechPlanPreview = {
|
|
46
|
+
available: false;
|
|
47
|
+
behavior: 'preview';
|
|
48
|
+
reason: string;
|
|
49
|
+
preview: Omit<TechPlanGraph, 'available'>;
|
|
50
|
+
nextActions: readonly string[];
|
|
51
|
+
};
|
|
52
|
+
export type TechPlanResult = TechPlanGraph | TechPlanPreview;
|
|
53
|
+
export type TechStatus = {
|
|
54
|
+
changeId: string;
|
|
55
|
+
status: TechStatusValue;
|
|
56
|
+
artifactRoot: string;
|
|
57
|
+
requiredArtifacts: string[];
|
|
58
|
+
missingArtifacts: string[];
|
|
59
|
+
approvalRecord: string | null;
|
|
60
|
+
blockedReasons: string[];
|
|
61
|
+
nextActions: string[];
|
|
62
|
+
};
|
|
63
|
+
export declare const TECH_REQUIRED_ARTIFACTS: readonly string[];
|
|
64
|
+
export declare function createTechPlan(request: TechPlanRequest): TechPlanResult;
|
|
65
|
+
export declare function getTechStatus(options: {
|
|
66
|
+
changeId: string;
|
|
67
|
+
artifactWorkspacePath?: string;
|
|
68
|
+
workspace?: WorkspaceConfig;
|
|
69
|
+
}): TechStatus;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { isInsidePath, stableRealPath } from '../../shared/path-utils.js';
|
|
4
|
+
import { buildArtifactRelativePath, validateChangeIdOrThrow } from '../../shared/change-id.js';
|
|
5
|
+
import { WORKSPACE_UNAVAILABLE_NEXT_ACTIONS } from '../../shared/planner-response.js';
|
|
6
|
+
import { hasValidArtifactWorkspace } from '../artifacts/workspace-service.js';
|
|
7
|
+
export const TECH_REQUIRED_ARTIFACTS = Object.freeze([
|
|
8
|
+
'frontend-tech-doc.md',
|
|
9
|
+
'backend-tech-doc.md',
|
|
10
|
+
'contract-tech-doc.md',
|
|
11
|
+
'test-tech-doc.md',
|
|
12
|
+
'platform-tech-doc.md',
|
|
13
|
+
'security-tech-doc.md',
|
|
14
|
+
'ci-tech-doc.md',
|
|
15
|
+
'migration-tech-doc.md',
|
|
16
|
+
'tech-review-report.md',
|
|
17
|
+
'tech-approval-record.md',
|
|
18
|
+
]);
|
|
19
|
+
const TECH_WAVE_TASKS = [
|
|
20
|
+
{ name: 'scan', taskIds: ['tech-architecture-scan', 'tech-frontend-scan', 'tech-backend-scan', 'tech-contract-scan', 'tech-test-scan', 'tech-platform-scan', 'tech-security-scan', 'tech-ci-scan'] },
|
|
21
|
+
{ name: 'document', taskIds: ['tech-frontend-doc-worker', 'tech-backend-doc-worker', 'tech-contract-doc-worker', 'tech-test-doc-worker', 'tech-platform-doc-worker', 'tech-security-doc-worker', 'tech-ci-doc-worker', 'tech-migration-doc-worker'] },
|
|
22
|
+
{ name: 'review', taskIds: ['tech-architecture-reviewer', 'tech-contract-reviewer', 'tech-security-reviewer', 'tech-test-reviewer', 'tech-platform-reviewer', 'tech-risk-reviewer'] },
|
|
23
|
+
{ name: 'reducer', taskIds: ['tech-reducer'] },
|
|
24
|
+
];
|
|
25
|
+
function assertNonEmptyGoal(goal) {
|
|
26
|
+
if (goal.trim().length === 0) {
|
|
27
|
+
throw new Error('Goal must be non-empty');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function architectureRoot(changeId) {
|
|
31
|
+
return buildArtifactRelativePath(changeId, 'architecture');
|
|
32
|
+
}
|
|
33
|
+
function hasPlannerArtifactWorkspace(artifactWorkspacePath, workspace) {
|
|
34
|
+
return !!workspace && hasValidArtifactWorkspace(workspace, artifactWorkspacePath);
|
|
35
|
+
}
|
|
36
|
+
function isEscapedArchitectureRoot(rootPath, artifactWorkspacePath) {
|
|
37
|
+
if (!existsSync(rootPath)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
return !isInsidePath(stableRealPath(rootPath), stableRealPath(artifactWorkspacePath));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function isValidArtifactFile(rootPath, artifact) {
|
|
48
|
+
const artifactPath = resolve(rootPath, artifact);
|
|
49
|
+
try {
|
|
50
|
+
const artifactStat = lstatSync(artifactPath);
|
|
51
|
+
if (artifactStat.isSymbolicLink())
|
|
52
|
+
return false;
|
|
53
|
+
if (!artifactStat.isFile())
|
|
54
|
+
return false;
|
|
55
|
+
if (!isInsidePath(stableRealPath(artifactPath), stableRealPath(rootPath)))
|
|
56
|
+
return false;
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function waveManifestPath(changeId, index, wave) {
|
|
64
|
+
return buildArtifactRelativePath(changeId, 'architecture', 'waves', `wave-${index + 1}-${wave}.json`);
|
|
65
|
+
}
|
|
66
|
+
function taskPurpose(taskId, goal) {
|
|
67
|
+
return `${taskId.replace(/^tech-/, '').replace(/-/g, ' ')} for ${goal}`;
|
|
68
|
+
}
|
|
69
|
+
function createTechGraph(request) {
|
|
70
|
+
validateChangeIdOrThrow(request.changeId);
|
|
71
|
+
assertNonEmptyGoal(request.goal);
|
|
72
|
+
const [scanWave, documentWave, reviewWave] = TECH_WAVE_TASKS;
|
|
73
|
+
const waves = TECH_WAVE_TASKS.map((wave) => ({ name: wave.name, taskIds: [...wave.taskIds] }));
|
|
74
|
+
const scanTaskIds = scanWave.taskIds;
|
|
75
|
+
const documentTaskIds = documentWave.taskIds;
|
|
76
|
+
const reviewTaskIds = reviewWave.taskIds;
|
|
77
|
+
const tasks = TECH_WAVE_TASKS.flatMap((wave) => wave.taskIds.map((taskId) => {
|
|
78
|
+
const dependsOn = wave.name === 'scan'
|
|
79
|
+
? []
|
|
80
|
+
: wave.name === 'document'
|
|
81
|
+
? [...scanTaskIds]
|
|
82
|
+
: wave.name === 'review'
|
|
83
|
+
? [...documentTaskIds]
|
|
84
|
+
: [...reviewTaskIds];
|
|
85
|
+
const briefPath = buildArtifactRelativePath(request.changeId, 'architecture', 'workers', taskId, 'brief.md');
|
|
86
|
+
return {
|
|
87
|
+
taskId,
|
|
88
|
+
wave: wave.name,
|
|
89
|
+
workerKind: taskId,
|
|
90
|
+
purpose: taskPurpose(taskId, request.goal),
|
|
91
|
+
inputs: [request.goal, architectureRoot(request.changeId)],
|
|
92
|
+
outputs: [briefPath],
|
|
93
|
+
dependsOn,
|
|
94
|
+
conflictGroup: `tech-${wave.name}`,
|
|
95
|
+
briefPath,
|
|
96
|
+
};
|
|
97
|
+
}));
|
|
98
|
+
return {
|
|
99
|
+
changeId: request.changeId,
|
|
100
|
+
goal: request.goal,
|
|
101
|
+
swarm: request.swarm,
|
|
102
|
+
dryRun: true,
|
|
103
|
+
artifactRoot: architectureRoot(request.changeId),
|
|
104
|
+
waves,
|
|
105
|
+
tasks,
|
|
106
|
+
outputs: {
|
|
107
|
+
taskGraph: buildArtifactRelativePath(request.changeId, 'architecture', 'tech-task-graph.json'),
|
|
108
|
+
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'),
|
|
111
|
+
},
|
|
112
|
+
blockedReasons: [],
|
|
113
|
+
nextActions: [],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export function createTechPlan(request) {
|
|
117
|
+
const graph = createTechGraph(request);
|
|
118
|
+
if (!request.artifactWorkspacePath || !hasPlannerArtifactWorkspace(request.artifactWorkspacePath, request.workspace)) {
|
|
119
|
+
return {
|
|
120
|
+
available: false,
|
|
121
|
+
behavior: 'preview',
|
|
122
|
+
reason: 'artifact-workspace-unavailable',
|
|
123
|
+
preview: graph,
|
|
124
|
+
nextActions: [...WORKSPACE_UNAVAILABLE_NEXT_ACTIONS],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
available: true,
|
|
129
|
+
...graph,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export function getTechStatus(options) {
|
|
133
|
+
validateChangeIdOrThrow(options.changeId);
|
|
134
|
+
const artifactRoot = architectureRoot(options.changeId);
|
|
135
|
+
if (!options.artifactWorkspacePath) {
|
|
136
|
+
return {
|
|
137
|
+
changeId: options.changeId,
|
|
138
|
+
status: 'unavailable',
|
|
139
|
+
artifactRoot,
|
|
140
|
+
requiredArtifacts: [...TECH_REQUIRED_ARTIFACTS],
|
|
141
|
+
missingArtifacts: [...TECH_REQUIRED_ARTIFACTS],
|
|
142
|
+
approvalRecord: null,
|
|
143
|
+
blockedReasons: ['artifact-workspace-unavailable'],
|
|
144
|
+
nextActions: [...WORKSPACE_UNAVAILABLE_NEXT_ACTIONS],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (!hasPlannerArtifactWorkspace(options.artifactWorkspacePath, options.workspace)) {
|
|
148
|
+
return {
|
|
149
|
+
changeId: options.changeId,
|
|
150
|
+
status: 'unavailable',
|
|
151
|
+
artifactRoot,
|
|
152
|
+
requiredArtifacts: [...TECH_REQUIRED_ARTIFACTS],
|
|
153
|
+
missingArtifacts: [...TECH_REQUIRED_ARTIFACTS],
|
|
154
|
+
approvalRecord: null,
|
|
155
|
+
blockedReasons: ['artifact-workspace-unavailable'],
|
|
156
|
+
nextActions: [...WORKSPACE_UNAVAILABLE_NEXT_ACTIONS],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const rootPath = resolve(options.artifactWorkspacePath, '.peaks', 'changes', options.changeId, 'architecture');
|
|
160
|
+
const approvalRecord = buildArtifactRelativePath(options.changeId, 'architecture', 'tech-approval-record.md');
|
|
161
|
+
if (isEscapedArchitectureRoot(rootPath, options.artifactWorkspacePath)) {
|
|
162
|
+
return {
|
|
163
|
+
changeId: options.changeId,
|
|
164
|
+
status: 'blocked',
|
|
165
|
+
artifactRoot,
|
|
166
|
+
requiredArtifacts: [...TECH_REQUIRED_ARTIFACTS],
|
|
167
|
+
missingArtifacts: [...TECH_REQUIRED_ARTIFACTS],
|
|
168
|
+
approvalRecord: null,
|
|
169
|
+
blockedReasons: ['tech-artifacts-missing'],
|
|
170
|
+
nextActions: ['Run peaks tech plan --dry-run, then persist and review the required tech artifacts.'],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const missingArtifacts = TECH_REQUIRED_ARTIFACTS.filter((artifact) => !existsSync(join(rootPath, artifact)) || !isValidArtifactFile(rootPath, artifact));
|
|
174
|
+
if (missingArtifacts.length === 1 && missingArtifacts[0] === 'tech-approval-record.md') {
|
|
175
|
+
return {
|
|
176
|
+
changeId: options.changeId,
|
|
177
|
+
status: 'blocked',
|
|
178
|
+
artifactRoot,
|
|
179
|
+
requiredArtifacts: [...TECH_REQUIRED_ARTIFACTS],
|
|
180
|
+
missingArtifacts,
|
|
181
|
+
approvalRecord: null,
|
|
182
|
+
blockedReasons: ['tech-approval-missing'],
|
|
183
|
+
nextActions: ['Create tech-approval-record.md with status: approved after review.'],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (missingArtifacts.length > 0) {
|
|
187
|
+
return {
|
|
188
|
+
changeId: options.changeId,
|
|
189
|
+
status: missingArtifacts.length === TECH_REQUIRED_ARTIFACTS.length ? 'missing' : 'blocked',
|
|
190
|
+
artifactRoot,
|
|
191
|
+
requiredArtifacts: [...TECH_REQUIRED_ARTIFACTS],
|
|
192
|
+
missingArtifacts,
|
|
193
|
+
approvalRecord: missingArtifacts.includes('tech-approval-record.md') ? null : approvalRecord,
|
|
194
|
+
blockedReasons: ['tech-artifacts-missing'],
|
|
195
|
+
nextActions: ['Run peaks tech plan --dry-run, then persist and review the required tech artifacts.'],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
let approvalContent;
|
|
199
|
+
try {
|
|
200
|
+
approvalContent = readFileSync(join(rootPath, 'tech-approval-record.md'), 'utf8');
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return {
|
|
204
|
+
changeId: options.changeId,
|
|
205
|
+
status: 'blocked',
|
|
206
|
+
artifactRoot,
|
|
207
|
+
requiredArtifacts: [...TECH_REQUIRED_ARTIFACTS],
|
|
208
|
+
missingArtifacts,
|
|
209
|
+
approvalRecord,
|
|
210
|
+
blockedReasons: ['tech-approval-unreadable'],
|
|
211
|
+
nextActions: ['Ensure tech-approval-record.md is readable and contains status: approved.'],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (!approvalContent.split(/\r?\n/).some((line) => line.trim() === 'status: approved')) {
|
|
215
|
+
return {
|
|
216
|
+
changeId: options.changeId,
|
|
217
|
+
status: 'blocked',
|
|
218
|
+
artifactRoot,
|
|
219
|
+
requiredArtifacts: [...TECH_REQUIRED_ARTIFACTS],
|
|
220
|
+
missingArtifacts,
|
|
221
|
+
approvalRecord,
|
|
222
|
+
blockedReasons: ['tech-approval-not-approved'],
|
|
223
|
+
nextActions: ['Update tech-approval-record.md with status: approved after review.'],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
changeId: options.changeId,
|
|
228
|
+
status: 'approved',
|
|
229
|
+
artifactRoot,
|
|
230
|
+
requiredArtifacts: [...TECH_REQUIRED_ARTIFACTS],
|
|
231
|
+
missingArtifacts: [],
|
|
232
|
+
approvalRecord,
|
|
233
|
+
blockedReasons: [],
|
|
234
|
+
nextActions: [],
|
|
235
|
+
};
|
|
236
|
+
}
|