peaks-cli 1.3.8 → 1.4.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/dist/src/cli/commands/core-artifact-commands.js +27 -0
- package/dist/src/cli/commands/project-commands.js +58 -1
- package/dist/src/cli/commands/request-commands.js +93 -3
- package/dist/src/cli/commands/retrospective-commands.d.ts +3 -0
- package/dist/src/cli/commands/retrospective-commands.js +113 -0
- package/dist/src/cli/commands/skill-scope-commands.d.ts +49 -0
- package/dist/src/cli/commands/skill-scope-commands.js +305 -0
- package/dist/src/cli/commands/workflow-commands.js +1 -1
- package/dist/src/cli/commands/workflow-plan-commands.d.ts +39 -0
- package/dist/src/cli/commands/workflow-plan-commands.js +163 -0
- package/dist/src/cli/program.js +8 -0
- package/dist/src/services/doctor/doctor-service.d.ts +40 -0
- package/dist/src/services/doctor/doctor-service.js +160 -0
- package/dist/src/services/hooks/presence-marker-detector.d.ts +16 -0
- package/dist/src/services/hooks/presence-marker-detector.js +105 -0
- package/dist/src/services/memory/project-memory-service.d.ts +19 -0
- package/dist/src/services/memory/project-memory-service.js +33 -0
- package/dist/src/services/retrospective/migrate-from-md.d.ts +37 -0
- package/dist/src/services/retrospective/migrate-from-md.js +528 -0
- package/dist/src/services/retrospective/retrospective-index.d.ts +37 -0
- package/dist/src/services/retrospective/retrospective-index.js +110 -0
- package/dist/src/services/retrospective/retrospective-show.d.ts +40 -0
- package/dist/src/services/retrospective/retrospective-show.js +109 -0
- package/dist/src/services/skill-scope/adapters/_stub-helper.d.ts +39 -0
- package/dist/src/services/skill-scope/adapters/_stub-helper.js +98 -0
- package/dist/src/services/skill-scope/adapters/claude-code.d.ts +59 -0
- package/dist/src/services/skill-scope/adapters/claude-code.js +304 -0
- package/dist/src/services/skill-scope/adapters/codex.d.ts +2 -0
- package/dist/src/services/skill-scope/adapters/codex.js +12 -0
- package/dist/src/services/skill-scope/adapters/cursor.d.ts +2 -0
- package/dist/src/services/skill-scope/adapters/cursor.js +13 -0
- package/dist/src/services/skill-scope/adapters/qoder.d.ts +2 -0
- package/dist/src/services/skill-scope/adapters/qoder.js +13 -0
- package/dist/src/services/skill-scope/adapters/tongyi.d.ts +2 -0
- package/dist/src/services/skill-scope/adapters/tongyi.js +13 -0
- package/dist/src/services/skill-scope/adapters/trae.d.ts +2 -0
- package/dist/src/services/skill-scope/adapters/trae.js +12 -0
- package/dist/src/services/skill-scope/detect.d.ts +75 -0
- package/dist/src/services/skill-scope/detect.js +480 -0
- package/dist/src/services/skill-scope/registry.d.ts +41 -0
- package/dist/src/services/skill-scope/registry.js +83 -0
- package/dist/src/services/skill-scope/source-of-truth.d.ts +44 -0
- package/dist/src/services/skill-scope/source-of-truth.js +118 -0
- package/dist/src/services/skill-scope/types.d.ts +176 -0
- package/dist/src/services/skill-scope/types.js +74 -0
- package/dist/src/services/standards/migrate-service.d.ts +63 -0
- package/dist/src/services/standards/migrate-service.js +193 -0
- package/dist/src/services/standards/project-standards-service.js +1 -23
- package/dist/src/services/workflow/artifact-paths.d.ts +59 -0
- package/dist/src/services/workflow/artifact-paths.js +127 -0
- package/dist/src/services/workflow/pipeline-verify-service.d.ts +6 -0
- package/dist/src/services/workflow/pipeline-verify-service.js +49 -4
- package/dist/src/services/workflow/plan-reader.d.ts +29 -0
- package/dist/src/services/workflow/plan-reader.js +158 -0
- package/dist/src/services/workflow/plan-refresher.d.ts +32 -0
- package/dist/src/services/workflow/plan-refresher.js +353 -0
- package/dist/src/services/workflow/plan-trigger-detector.d.ts +55 -0
- package/dist/src/services/workflow/plan-trigger-detector.js +142 -0
- package/dist/src/shared/format-md-compact.d.ts +32 -0
- package/dist/src/shared/format-md-compact.js +297 -0
- package/dist/src/shared/stale-policy.d.ts +67 -0
- package/dist/src/shared/stale-policy.js +85 -0
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +3 -2
- package/schemas/doctor-report.schema.json +2 -2
- package/skills/peaks-qa/SKILL.md +103 -507
- package/skills/peaks-qa/references/artifact-per-request.md +7 -79
- package/skills/peaks-qa/references/browser-validation-contracts.md +51 -0
- package/skills/peaks-qa/references/codegraph-regression-focus.md +5 -0
- package/skills/peaks-qa/references/external-capability-guidance.md +9 -0
- package/skills/peaks-qa/references/qa-compact-handoff.md +3 -0
- package/skills/peaks-qa/references/qa-context-governance.md +24 -0
- package/skills/peaks-qa/references/qa-fanout-contract.md +8 -0
- package/skills/peaks-qa/references/qa-gstack-integration.md +7 -0
- package/skills/peaks-qa/references/qa-local-artifacts.md +3 -0
- package/skills/peaks-qa/references/qa-matt-pocock-integration.md +9 -0
- package/skills/peaks-qa/references/qa-perf-test-plan.md +67 -0
- package/skills/peaks-qa/references/qa-refactor-role.md +3 -0
- package/skills/peaks-qa/references/qa-runbook.md +74 -0
- package/skills/peaks-qa/references/qa-security-test-plan.md +73 -0
- package/skills/peaks-qa/references/qa-skill-presence.md +22 -0
- package/skills/peaks-qa/references/qa-standards-preflight.md +8 -0
- package/skills/peaks-qa/references/qa-sub-agent-dispatch.md +38 -0
- package/skills/peaks-qa/references/qa-transition-gates.md +83 -0
- package/skills/peaks-qa/references/requirement-boundary-recheck.md +9 -0
- package/skills/peaks-qa/references/test-case-generation.md +27 -0
- package/skills/peaks-qa/references/test-report-output.md +14 -0
- package/skills/peaks-rd/SKILL.md +85 -612
- package/skills/peaks-rd/references/artifact-and-standards-output.md +9 -0
- package/skills/peaks-rd/references/artifact-per-request.md +20 -0
- package/skills/peaks-rd/references/browser-self-test-contracts.md +29 -0
- package/skills/peaks-rd/references/codegraph-project-analysis.md +5 -0
- package/skills/peaks-rd/references/compact-handoff.md +3 -0
- package/skills/peaks-rd/references/external-references.md +11 -0
- package/skills/peaks-rd/references/frontend-project-generation.md +11 -0
- package/skills/peaks-rd/references/library-version-awareness.md +30 -0
- package/skills/peaks-rd/references/mandatory-perf-baseline.md +42 -0
- package/skills/peaks-rd/references/mandatory-tech-doc.md +18 -0
- package/skills/peaks-rd/references/matt-pocock-integration.md +11 -0
- package/skills/peaks-rd/references/mock-data-placement.md +40 -0
- package/skills/peaks-rd/references/parallel-review-fanout.md +81 -0
- package/skills/peaks-rd/references/rd-context-governance.md +36 -0
- package/skills/peaks-rd/references/rd-gstack-integration.md +16 -0
- package/skills/peaks-rd/references/rd-runbook.md +125 -0
- package/skills/peaks-rd/references/rd-standards-preflight.md +8 -0
- package/skills/peaks-rd/references/rd-sub-agent-dispatch.md +39 -0
- package/skills/peaks-rd/references/rd-transition-gates.md +1 -1
- package/skills/peaks-rd/references/skill-presence-and-title.md +22 -0
- package/skills/peaks-solo/SKILL.md +87 -595
- package/skills/peaks-solo/references/anchoring-and-session-info.md +25 -0
- package/skills/peaks-solo/references/boundaries.md +21 -0
- package/skills/peaks-solo/references/codegraph-orchestration.md +5 -0
- package/skills/peaks-solo/references/completion-handoff.md +16 -0
- package/skills/peaks-solo/references/context-governance.md +51 -0
- package/skills/peaks-solo/references/external-references.md +17 -0
- package/skills/peaks-solo/references/frontend-only-mode.md +14 -0
- package/skills/peaks-solo/references/gstack-integration.md +7 -0
- package/skills/peaks-solo/references/local-artifact-workspace.md +79 -0
- package/skills/peaks-solo/references/micro-cycle.md +68 -0
- package/skills/peaks-solo/references/mode-selection.md +21 -0
- package/skills/peaks-solo/references/openspec-workflow.md +43 -0
- package/skills/peaks-solo/references/project-memory-loading.md +17 -0
- package/skills/peaks-solo/references/quality-gate-cheatsheet.md +13 -0
- package/skills/peaks-solo/references/resume-detection.md +63 -0
- package/skills/peaks-solo/references/runbook.md +1 -1
- package/skills/peaks-solo/references/skill-presence-and-title.md +31 -0
- package/skills/peaks-solo/references/standards-preflight.md +23 -0
- package/skills/peaks-solo/references/sub-agent-dispatch.md +46 -0
- package/skills/peaks-solo/references/swarm-dispatch-contract.md +56 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Slice 028 (Q2=A): `peaks standards migrate` rewrites a consumer
|
|
5
|
+
* project's `CLAUDE.md` to drop the legacy heartbeat block.
|
|
6
|
+
*
|
|
7
|
+
* The legacy block (rendered by `peaks standards init` / `peaks
|
|
8
|
+
* standards update` before slice 028) contained instructions that:
|
|
9
|
+
* 1. pointed the LLM at the legacy `.peaks/.active-skill.json` path;
|
|
10
|
+
* 2. required the LLM to invoke `peaks skill heartbeat:touch` and
|
|
11
|
+
* `peaks skill presence:clear` on every turn;
|
|
12
|
+
* 3. ended with an `External reference: https://github.com/affaan-m/...`
|
|
13
|
+
* line that was peaks-cli-internal, not consumer-facing.
|
|
14
|
+
*
|
|
15
|
+
* The replacement text matches the post-slice-025 peaks-cli repo's own
|
|
16
|
+
* `CLAUDE.md`: route the LLM through `peaks skill presence --json` and
|
|
17
|
+
* render a compact status header when a valid skill is active. This
|
|
18
|
+
* service is the deterministic in-place rewriter that brings existing
|
|
19
|
+
* consumer trees in line with the new template.
|
|
20
|
+
*
|
|
21
|
+
* Behavior:
|
|
22
|
+
* - `migrateStandards({ project, apply: true })` — rewrites the
|
|
23
|
+
* file when the legacy block is present, returns
|
|
24
|
+
* `applied: true`.
|
|
25
|
+
* - `migrateStandards({ project, dryRun: true })` — returns the
|
|
26
|
+
* would-change preview, no write.
|
|
27
|
+
* - `migrateStandards({ project })` — defaults to a
|
|
28
|
+
* dry-run. `--apply` is the only opt-in for a destructive write.
|
|
29
|
+
* - File missing → `file: null`, no throw.
|
|
30
|
+
* - Legacy block not present → `foundOldBlock: false`, no write.
|
|
31
|
+
*/
|
|
32
|
+
export const NEW_TEMPLATE_TEXT = 'Active Peaks-Cli skill presence: at the start of every response, run `peaks skill presence --json` to read the active skill marker. The CLI handles canonical-path resolution (`.peaks/_runtime/active-skill.json` with back-compat fallback to `.peaks/.active-skill.json`); do not read those files directly. When the response includes a valid skill name, display the compact status header: `Peaks-Cli Skill: <skill> | Peaks-Cli Gate: <gate> | Next: <one short action>`. Display the header on every turn while the CLI returns an active skill; omit when the CLI returns no active skill.';
|
|
33
|
+
const LEGACY_BLOCK_OPENER_LINE = 'Peaks-Cli 心跳检测 (heartbeat check)';
|
|
34
|
+
const LEGACY_BLOCK_CLOSER = 'External reference: https://github.com/affaan-m/everything-claude-code';
|
|
35
|
+
const LEGACY_MARKER_FALLBACK = 'Do NOT skip step 3-5. The CLI heartbeat:touch command';
|
|
36
|
+
const FORBIDDEN_LEGACY_STRINGS = [
|
|
37
|
+
'heartbeat:touch',
|
|
38
|
+
'presence:clear',
|
|
39
|
+
'Default runbook',
|
|
40
|
+
'Startup sequence',
|
|
41
|
+
'Swarm parallel phase'
|
|
42
|
+
];
|
|
43
|
+
const NEW_TEMPLATE_FINGERPRINT = 'peaks skill presence --json';
|
|
44
|
+
export function detectLegacyBlock(content) {
|
|
45
|
+
const openerIndex = content.indexOf(LEGACY_BLOCK_OPENER_LINE);
|
|
46
|
+
if (openerIndex >= 0) {
|
|
47
|
+
// Walk back to the start of the `<!--` line. The opener text is
|
|
48
|
+
// typically indented on the line after `<!--` (the legacy block
|
|
49
|
+
// is a multi-line HTML comment), so we cut from the most recent
|
|
50
|
+
// `<!--` line above. If no `<!--` is found in the surrounding
|
|
51
|
+
// 4 lines, fall back to the start of the opener's own line.
|
|
52
|
+
const htmlCommentIndex = content.lastIndexOf('<!--', openerIndex);
|
|
53
|
+
const previousNewlineBeforeOpener = content.lastIndexOf('\n', openerIndex);
|
|
54
|
+
let start;
|
|
55
|
+
if (htmlCommentIndex < 0 || htmlCommentIndex < previousNewlineBeforeOpener - 200) {
|
|
56
|
+
// No `<!--` line within a reasonable distance — start of the
|
|
57
|
+
// opener's own line.
|
|
58
|
+
start = previousNewlineBeforeOpener + 1;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
start = content.lastIndexOf('\n', htmlCommentIndex) + 1;
|
|
62
|
+
}
|
|
63
|
+
const closerIndex = content.indexOf(LEGACY_BLOCK_CLOSER, openerIndex);
|
|
64
|
+
let endIndex;
|
|
65
|
+
if (closerIndex < 0) {
|
|
66
|
+
const tailIndex = content.indexOf(LEGACY_MARKER_FALLBACK, openerIndex);
|
|
67
|
+
if (tailIndex < 0) {
|
|
68
|
+
endIndex = content.length;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
endIndex = content.indexOf('\n', tailIndex);
|
|
72
|
+
if (endIndex < 0)
|
|
73
|
+
endIndex = content.length;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
endIndex = content.indexOf('\n', closerIndex);
|
|
78
|
+
if (endIndex < 0)
|
|
79
|
+
endIndex = content.length;
|
|
80
|
+
}
|
|
81
|
+
return { found: true, start, end: endIndex };
|
|
82
|
+
}
|
|
83
|
+
// Fallback: opener stripped by editor / re-format. Detect by the
|
|
84
|
+
// first forbidden string that survives the rewrite, then walk back
|
|
85
|
+
// to the start of the line.
|
|
86
|
+
for (const marker of FORBIDDEN_LEGACY_STRINGS) {
|
|
87
|
+
const idx = content.indexOf(marker);
|
|
88
|
+
if (idx >= 0) {
|
|
89
|
+
const start = content.lastIndexOf('\n', idx) + 1;
|
|
90
|
+
return { found: true, start, end: content.length };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { found: false, start: -1, end: -1 };
|
|
94
|
+
}
|
|
95
|
+
export function rewriteLegacyBlock(content, newText = NEW_TEMPLATE_TEXT) {
|
|
96
|
+
const detection = detectLegacyBlock(content);
|
|
97
|
+
if (!detection.found) {
|
|
98
|
+
return { rewritten: content, replaced: false };
|
|
99
|
+
}
|
|
100
|
+
const before = content.slice(0, detection.start);
|
|
101
|
+
const after = content.slice(detection.end);
|
|
102
|
+
const trimmedBefore = before.replace(/\s+$/u, '\n');
|
|
103
|
+
const cleanedAfter = after.replace(/^\n+/u, '\n');
|
|
104
|
+
const rewritten = `${trimmedBefore}${newText}${cleanedAfter}`;
|
|
105
|
+
return { rewritten, replaced: true };
|
|
106
|
+
}
|
|
107
|
+
export function migrateStandards(input) {
|
|
108
|
+
const project = input.project;
|
|
109
|
+
const projectRoot = isAbsolute(project) ? project : resolve(project);
|
|
110
|
+
const filePath = resolve(projectRoot, 'CLAUDE.md');
|
|
111
|
+
const apply = input.apply === true;
|
|
112
|
+
const dryRun = input.dryRun === true || apply === false;
|
|
113
|
+
if (!existsSync(filePath)) {
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
data: {
|
|
117
|
+
file: null,
|
|
118
|
+
foundOldBlock: false,
|
|
119
|
+
wouldChange: false,
|
|
120
|
+
applied: false,
|
|
121
|
+
before: null,
|
|
122
|
+
after: null,
|
|
123
|
+
nextActions: ['CLAUDE.md does not exist; nothing to migrate']
|
|
124
|
+
},
|
|
125
|
+
warnings: []
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const original = readFileSync(filePath, 'utf8');
|
|
129
|
+
const detection = detectLegacyBlock(original);
|
|
130
|
+
if (!detection.found) {
|
|
131
|
+
if (original.includes(NEW_TEMPLATE_FINGERPRINT)) {
|
|
132
|
+
return {
|
|
133
|
+
ok: true,
|
|
134
|
+
data: {
|
|
135
|
+
file: filePath,
|
|
136
|
+
foundOldBlock: false,
|
|
137
|
+
wouldChange: false,
|
|
138
|
+
applied: false,
|
|
139
|
+
before: { lines: original.split('\n').length },
|
|
140
|
+
after: null,
|
|
141
|
+
nextActions: ['CLAUDE.md is already up to date']
|
|
142
|
+
},
|
|
143
|
+
warnings: []
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
ok: true,
|
|
148
|
+
data: {
|
|
149
|
+
file: filePath,
|
|
150
|
+
foundOldBlock: false,
|
|
151
|
+
wouldChange: false,
|
|
152
|
+
applied: false,
|
|
153
|
+
before: { lines: original.split('\n').length },
|
|
154
|
+
after: null,
|
|
155
|
+
nextActions: ['CLAUDE.md has no peaks-cli block; nothing to migrate']
|
|
156
|
+
},
|
|
157
|
+
warnings: []
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const { rewritten } = rewriteLegacyBlock(original);
|
|
161
|
+
const nextActions = [];
|
|
162
|
+
if (dryRun) {
|
|
163
|
+
nextActions.push('Re-run with --apply to perform the rewrite');
|
|
164
|
+
return {
|
|
165
|
+
ok: true,
|
|
166
|
+
data: {
|
|
167
|
+
file: filePath,
|
|
168
|
+
foundOldBlock: true,
|
|
169
|
+
wouldChange: true,
|
|
170
|
+
applied: false,
|
|
171
|
+
before: { lines: original.split('\n').length },
|
|
172
|
+
after: { lines: rewritten.split('\n').length },
|
|
173
|
+
nextActions
|
|
174
|
+
},
|
|
175
|
+
warnings: []
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
writeFileSync(filePath, rewritten, 'utf8');
|
|
179
|
+
nextActions.push('CLAUDE.md rewritten; no further action required');
|
|
180
|
+
return {
|
|
181
|
+
ok: true,
|
|
182
|
+
data: {
|
|
183
|
+
file: filePath,
|
|
184
|
+
foundOldBlock: true,
|
|
185
|
+
wouldChange: true,
|
|
186
|
+
applied: true,
|
|
187
|
+
before: { lines: original.split('\n').length },
|
|
188
|
+
after: { lines: rewritten.split('\n').length },
|
|
189
|
+
nextActions
|
|
190
|
+
},
|
|
191
|
+
warnings: []
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -134,29 +134,7 @@ function renderClaudeMd(language, ctx) {
|
|
|
134
134
|
'- Read `.claude/rules/common/security.md` before touching filesystem, user input, external calls, auth, or secrets.',
|
|
135
135
|
`- Read .claude/rules/${language}/coding-style.md for language-specific standards when applicable.`,
|
|
136
136
|
'',
|
|
137
|
-
'
|
|
138
|
-
' Peaks-Cli 心跳检测 (heartbeat check)',
|
|
139
|
-
' LLM 每轮回复前必须执行以下步骤,由 CLI 命令强制执行:',
|
|
140
|
-
' 1. 读取 .peaks/.active-skill.json → 存在则显示 status header',
|
|
141
|
-
' 2. 扫描当前上下文查找 peaks 技能标记',
|
|
142
|
-
' ("Peaks-Cli Gate", "Peaks-Cli Default runbook", "Peaks-Cli Startup sequence")',
|
|
143
|
-
' 3. 标记存在 → 调用 `peaks skill heartbeat:touch` 记录心跳',
|
|
144
|
-
' 4. 标记不存在 → 显示警告 + 调用 `peaks skill presence:clear` 关闭心跳',
|
|
145
|
-
'-->',
|
|
146
|
-
'Active Peaks-Cli skill presence: at the start of EVERY response, run these steps in order:',
|
|
147
|
-
'',
|
|
148
|
-
'1. Read `.peaks/.active-skill.json`. If missing or invalid, skip all remaining steps.',
|
|
149
|
-
'2. Display the compact status header: `Peaks-Cli Skill: <skill> | Peaks-Cli Gate: <gate> | Next: <one short action>`.',
|
|
150
|
-
'3. Scan the current conversation context for peaks skill markers: "Peaks-Cli Gate", "Peaks-Cli Default runbook", "Peaks-Cli Startup sequence", "Peaks-Cli Swarm parallel phase".',
|
|
151
|
-
'4. If at least one marker IS found → run `peaks skill heartbeat:touch` to record a successful heartbeat. The CLI command writes the timestamp and returns JSON — verify it succeeded.',
|
|
152
|
-
'5. If NO markers are found → the skill context has been lost (context compaction). Run `peaks skill presence:clear` to close the heartbeat, then display this warning:',
|
|
153
|
-
'',
|
|
154
|
-
'> ⚠ Peaks-Cli: skill presence file was active but skill context has been lost from the conversation. The workflow can no longer run correctly. Please re-invoke the relevant /peaks-* skill to reload the full skill instructions.',
|
|
155
|
-
'',
|
|
156
|
-
'Do NOT skip step 3-5. The CLI heartbeat:touch command is the mechanism that makes heartbeat auditable — failing to call it means the heartbeat is broken.',
|
|
157
|
-
'',
|
|
158
|
-
'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.',
|
|
159
|
-
''
|
|
137
|
+
'Active Peaks-Cli skill presence: at the start of every response, run `peaks skill presence --json` to read the active skill marker. The CLI handles canonical-path resolution (`.peaks/_runtime/active-skill.json` with back-compat fallback to `.peaks/.active-skill.json`); do not read those files directly. When the response includes a valid skill name, display the compact status header: `Peaks-Cli Skill: <skill> | Peaks-Cli Gate: <gate> | Next: <one short action>`. Display the header on every turn while the CLI returns an active skill; omit when the CLI returns no active skill.'
|
|
160
138
|
].join('\n');
|
|
161
139
|
const stack = renderProjectStackSection(ctx);
|
|
162
140
|
return stack === '' ? head : `${head}\n${stack}`;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** File name (with `<rid>` suffix) for the per-request security findings delta. */
|
|
2
|
+
export declare const SECURITY_FINDINGS_SUFFIXED: (rid: string) => string;
|
|
3
|
+
/** File name (legacy, no `<rid>` suffix) for the security findings artifact. */
|
|
4
|
+
export declare const SECURITY_FINDINGS_LEGACY = "security-findings.md";
|
|
5
|
+
/** File name (with `<rid>` suffix) for the per-request performance findings delta. */
|
|
6
|
+
export declare const PERFORMANCE_FINDINGS_SUFFIXED: (rid: string) => string;
|
|
7
|
+
/** File name (legacy, no `<rid>` suffix) for the performance findings artifact. */
|
|
8
|
+
export declare const PERFORMANCE_FINDINGS_LEGACY = "performance-findings.md";
|
|
9
|
+
export interface ResolveFindingsPathResult {
|
|
10
|
+
/** The resolved absolute path (suffixed preferred, legacy fallback). */
|
|
11
|
+
readonly path: string;
|
|
12
|
+
/** `'suffixed' | 'legacy' | 'legacy-redirect'` — useful for Gate C warnings. */
|
|
13
|
+
readonly form: 'suffixed' | 'legacy' | 'legacy-redirect';
|
|
14
|
+
/** When the consumer was redirected from a legacy to a suffixed path, the
|
|
15
|
+
* original legacy path. Otherwise null. */
|
|
16
|
+
readonly redirectedFrom: string | null;
|
|
17
|
+
/** The rid that was used to resolve the suffixed path, if any. */
|
|
18
|
+
readonly rid: string | null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the security-findings artifact path. Preferred form is the
|
|
22
|
+
* `<rid>`-suffixed path; legacy non-suffixed path is accepted as a
|
|
23
|
+
* 1-minor-release back-compat fallback.
|
|
24
|
+
*
|
|
25
|
+
* When `rid` is provided and the suffixed form is missing, the legacy
|
|
26
|
+
* form is reported (NOT the suffixed path) so the caller can decide to
|
|
27
|
+
* log a warning. When `rid` is undefined, the legacy form is the
|
|
28
|
+
* canonical target.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveSecurityFindingsPath(args: {
|
|
31
|
+
projectRoot: string;
|
|
32
|
+
changeId: string;
|
|
33
|
+
rid?: string;
|
|
34
|
+
}): ResolveFindingsPathResult;
|
|
35
|
+
/** Resolve the performance-findings artifact path (mirror of `resolveSecurityFindingsPath`). */
|
|
36
|
+
export declare function resolvePerformanceFindingsPath(args: {
|
|
37
|
+
projectRoot: string;
|
|
38
|
+
changeId: string;
|
|
39
|
+
rid?: string;
|
|
40
|
+
}): ResolveFindingsPathResult;
|
|
41
|
+
/**
|
|
42
|
+
* Lazy migration: rename a legacy non-suffixed QA artifact to the
|
|
43
|
+
* suffixed form for the given rid. Idempotent — re-running is a no-op
|
|
44
|
+
* once the suffixed form exists.
|
|
45
|
+
*
|
|
46
|
+
* Returns the resulting path. Callers (Gate C in `pipeline-verify-service.ts`)
|
|
47
|
+
* log a warning when the legacy form is the one consumed.
|
|
48
|
+
*/
|
|
49
|
+
export declare function lazyMigrateLegacyFindings(args: {
|
|
50
|
+
projectRoot: string;
|
|
51
|
+
changeId: string;
|
|
52
|
+
rid: string;
|
|
53
|
+
base: 'security-findings' | 'performance-findings';
|
|
54
|
+
legacyFile: string;
|
|
55
|
+
suffixedFile: (rid: string) => string;
|
|
56
|
+
}): {
|
|
57
|
+
renamed: boolean;
|
|
58
|
+
path: string;
|
|
59
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-slice artifact path resolvers (slice 025 — Security + Perf
|
|
3
|
+
* Plan/Result split).
|
|
4
|
+
*
|
|
5
|
+
* The plan/result split introduces per-request `<rid>`-suffixed QA
|
|
6
|
+
* artifacts (`security-findings-<rid>.md`, `performance-findings-<rid>.md`)
|
|
7
|
+
* in addition to the legacy non-suffixed form. This module is the single
|
|
8
|
+
* source of truth for the canonical and legacy paths and the lazy
|
|
9
|
+
* migration that bridges the 1-minor-release back-compat window.
|
|
10
|
+
*
|
|
11
|
+
* The QA artifacts live under the change-id dir (`.peaks/<changeId>/qa/`),
|
|
12
|
+
* which is the same dir Gate C has historically looked at. The
|
|
13
|
+
* `<sessionId>` argument is accepted for symmetry with the
|
|
14
|
+
* plan/result services (which DO use `.peaks/_runtime/<sessionId>/qa/`)
|
|
15
|
+
* but is unused here.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, renameSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
const QA_DIR = 'qa';
|
|
20
|
+
/** File name (with `<rid>` suffix) for the per-request security findings delta. */
|
|
21
|
+
export const SECURITY_FINDINGS_SUFFIXED = (rid) => `security-findings-${rid}.md`;
|
|
22
|
+
/** File name (legacy, no `<rid>` suffix) for the security findings artifact. */
|
|
23
|
+
export const SECURITY_FINDINGS_LEGACY = 'security-findings.md';
|
|
24
|
+
/** File name (with `<rid>` suffix) for the per-request performance findings delta. */
|
|
25
|
+
export const PERFORMANCE_FINDINGS_SUFFIXED = (rid) => `performance-findings-${rid}.md`;
|
|
26
|
+
/** File name (legacy, no `<rid>` suffix) for the performance findings artifact. */
|
|
27
|
+
export const PERFORMANCE_FINDINGS_LEGACY = 'performance-findings.md';
|
|
28
|
+
/** Base name (no extension) shared by both the suffixed and legacy forms. */
|
|
29
|
+
const SECURITY_FINDINGS_BASE = 'security-findings';
|
|
30
|
+
const PERFORMANCE_FINDINGS_BASE = 'performance-findings';
|
|
31
|
+
/**
|
|
32
|
+
* Try the most-specific read first: the suffixed form. On miss, fall back
|
|
33
|
+
* to the legacy form. On legacy hit, run a one-shot lazy migration that
|
|
34
|
+
* renames the legacy file to `<base>-<rid>.md` (when the legacy body
|
|
35
|
+
* contains a recognizable rid), or leaves a 3-line redirect stub.
|
|
36
|
+
*
|
|
37
|
+
* Pure path resolver: does NOT write any new content. The lazy migration
|
|
38
|
+
* only renames an existing file; it does not invent data.
|
|
39
|
+
*/
|
|
40
|
+
function resolveFindingsPath(args) {
|
|
41
|
+
const qaDir = join(args.projectRoot, '.peaks', args.changeId, QA_DIR);
|
|
42
|
+
if (args.rid !== undefined) {
|
|
43
|
+
const suffixedPath = join(qaDir, args.suffixedFile(args.rid));
|
|
44
|
+
if (existsSync(suffixedPath)) {
|
|
45
|
+
return { path: suffixedPath, form: 'suffixed', redirectedFrom: null, rid: args.rid };
|
|
46
|
+
}
|
|
47
|
+
const legacyPath = join(qaDir, args.legacyFile);
|
|
48
|
+
if (existsSync(legacyPath)) {
|
|
49
|
+
// Best-effort lazy migration: rename legacy → suffixed so subsequent
|
|
50
|
+
// reads hit the preferred form. We do NOT touch the file contents;
|
|
51
|
+
// an older file may not actually be "for" this rid. The caller is
|
|
52
|
+
// expected to treat the result as 'legacy' form, not 'suffixed'.
|
|
53
|
+
return {
|
|
54
|
+
path: legacyPath,
|
|
55
|
+
form: 'legacy',
|
|
56
|
+
redirectedFrom: null,
|
|
57
|
+
rid: null
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// No file present; report the would-be suffixed path so the caller can
|
|
61
|
+
// surface it in error messages.
|
|
62
|
+
return { path: suffixedPath, form: 'suffixed', redirectedFrom: null, rid: args.rid };
|
|
63
|
+
}
|
|
64
|
+
// rid is undefined — caller wants the legacy single-file form.
|
|
65
|
+
const legacyPath = join(qaDir, args.legacyFile);
|
|
66
|
+
if (existsSync(legacyPath)) {
|
|
67
|
+
return { path: legacyPath, form: 'legacy', redirectedFrom: null, rid: null };
|
|
68
|
+
}
|
|
69
|
+
return { path: legacyPath, form: 'legacy', redirectedFrom: null, rid: null };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the security-findings artifact path. Preferred form is the
|
|
73
|
+
* `<rid>`-suffixed path; legacy non-suffixed path is accepted as a
|
|
74
|
+
* 1-minor-release back-compat fallback.
|
|
75
|
+
*
|
|
76
|
+
* When `rid` is provided and the suffixed form is missing, the legacy
|
|
77
|
+
* form is reported (NOT the suffixed path) so the caller can decide to
|
|
78
|
+
* log a warning. When `rid` is undefined, the legacy form is the
|
|
79
|
+
* canonical target.
|
|
80
|
+
*/
|
|
81
|
+
export function resolveSecurityFindingsPath(args) {
|
|
82
|
+
return resolveFindingsPath({
|
|
83
|
+
projectRoot: args.projectRoot,
|
|
84
|
+
changeId: args.changeId,
|
|
85
|
+
...(args.rid !== undefined ? { rid: args.rid } : {}),
|
|
86
|
+
base: SECURITY_FINDINGS_BASE,
|
|
87
|
+
legacyFile: SECURITY_FINDINGS_LEGACY,
|
|
88
|
+
suffixedFile: SECURITY_FINDINGS_SUFFIXED
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/** Resolve the performance-findings artifact path (mirror of `resolveSecurityFindingsPath`). */
|
|
92
|
+
export function resolvePerformanceFindingsPath(args) {
|
|
93
|
+
return resolveFindingsPath({
|
|
94
|
+
projectRoot: args.projectRoot,
|
|
95
|
+
changeId: args.changeId,
|
|
96
|
+
...(args.rid !== undefined ? { rid: args.rid } : {}),
|
|
97
|
+
base: PERFORMANCE_FINDINGS_BASE,
|
|
98
|
+
legacyFile: PERFORMANCE_FINDINGS_LEGACY,
|
|
99
|
+
suffixedFile: PERFORMANCE_FINDINGS_SUFFIXED
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Lazy migration: rename a legacy non-suffixed QA artifact to the
|
|
104
|
+
* suffixed form for the given rid. Idempotent — re-running is a no-op
|
|
105
|
+
* once the suffixed form exists.
|
|
106
|
+
*
|
|
107
|
+
* Returns the resulting path. Callers (Gate C in `pipeline-verify-service.ts`)
|
|
108
|
+
* log a warning when the legacy form is the one consumed.
|
|
109
|
+
*/
|
|
110
|
+
export function lazyMigrateLegacyFindings(args) {
|
|
111
|
+
const qaDir = join(args.projectRoot, '.peaks', args.changeId, QA_DIR);
|
|
112
|
+
const legacyPath = join(qaDir, args.legacyFile);
|
|
113
|
+
const suffixedPath = join(qaDir, args.suffixedFile(args.rid));
|
|
114
|
+
if (!existsSync(legacyPath)) {
|
|
115
|
+
return { renamed: false, path: suffixedPath };
|
|
116
|
+
}
|
|
117
|
+
if (existsSync(suffixedPath)) {
|
|
118
|
+
return { renamed: false, path: suffixedPath };
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
renameSync(legacyPath, suffixedPath);
|
|
122
|
+
return { renamed: true, path: suffixedPath };
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return { renamed: false, path: legacyPath };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -22,6 +22,12 @@ export type PipelineVerification = {
|
|
|
22
22
|
};
|
|
23
23
|
violations: string[];
|
|
24
24
|
nextActions: string[];
|
|
25
|
+
/** Form of the security/performance findings artifacts Gate C accepted
|
|
26
|
+
* (slice 025). `'suffixed'` for the new per-rid form, `'legacy'` for the
|
|
27
|
+
* pre-slice-025 non-suffixed form, `'none'` when neither was found. */
|
|
28
|
+
acceptedForm?: 'suffixed' | 'legacy' | 'none';
|
|
29
|
+
/** `gateC` is the pre-computed verdict string (AC7 dogfood shape). */
|
|
30
|
+
gateC?: 'pass' | 'fail';
|
|
25
31
|
};
|
|
26
32
|
export declare function verifyPipeline(options: {
|
|
27
33
|
projectRoot: string;
|
|
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { isRequestType } from '../artifacts/artifact-prerequisites.js';
|
|
4
4
|
import { showRequestArtifact } from '../artifacts/request-artifact-service.js';
|
|
5
|
+
import { resolveSecurityFindingsPath, resolvePerformanceFindingsPath } from './artifact-paths.js';
|
|
5
6
|
function extractState(markdown) {
|
|
6
7
|
for (const rawLine of markdown.split(/\r?\n/)) {
|
|
7
8
|
const match = /^-\s*state:\s*(.+?)\s*$/.exec(rawLine.trim());
|
|
@@ -133,14 +134,40 @@ export async function verifyPipeline(options) {
|
|
|
133
134
|
nextActions.push('Invoke Skill(skill="peaks-qa") with the request-id for functional/performance/security testing');
|
|
134
135
|
qaGates[0].detail = 'not found';
|
|
135
136
|
}
|
|
136
|
-
// Check QA evidence files
|
|
137
|
+
// Check QA evidence files. For the security/performance findings
|
|
138
|
+
// gates, the canonical form post-slice-025 is the per-rid suffixed
|
|
139
|
+
// path; the legacy non-suffixed form is still accepted during the
|
|
140
|
+
// 1-minor-release back-compat window. The path resolver
|
|
141
|
+
// (artifact-paths.ts) decides which form to consume; we pass the
|
|
142
|
+
// resolved change-id and the rid.
|
|
143
|
+
const changeIdForResolver = resolvedChangeId || rdEvidenceDir;
|
|
137
144
|
const QA_EVIDENCE_FILE = {
|
|
138
145
|
'test-cases': `test-cases/${options.rid}.md`,
|
|
139
146
|
'test-report': `test-reports/${options.rid}.md`,
|
|
140
|
-
'security-findings': '
|
|
141
|
-
'performance-findings': '
|
|
147
|
+
'security-findings': '',
|
|
148
|
+
'performance-findings': ''
|
|
142
149
|
};
|
|
143
150
|
for (const gate of qaGates.slice(1)) {
|
|
151
|
+
if (gate.name === 'security-findings' || gate.name === 'performance-findings') {
|
|
152
|
+
const resolver = gate.name === 'security-findings' ? resolveSecurityFindingsPath : resolvePerformanceFindingsPath;
|
|
153
|
+
const resolved = resolver({ projectRoot: options.projectRoot, changeId: changeIdForResolver, rid: options.rid });
|
|
154
|
+
if (existsSync(resolved.path)) {
|
|
155
|
+
gate.passed = true;
|
|
156
|
+
gate.detail = resolved.path;
|
|
157
|
+
if (resolved.form === 'legacy') {
|
|
158
|
+
// 1-minor-release back-compat window. Surface the warning so
|
|
159
|
+
// users know to migrate. Per PRD §Migration the form will be
|
|
160
|
+
// rejected after the next minor bump.
|
|
161
|
+
violations.push(`QA evidence accepted in legacy form (will be rejected after next minor release): ${resolved.path} — re-run peaks workflow plan refresh to migrate`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
gate.detail = `missing: ${resolved.path}`;
|
|
166
|
+
violations.push(`QA evidence missing: ${gate.description} (${resolved.path})`);
|
|
167
|
+
nextActions.push(`Create ${resolved.path} (or use the legacy non-suffixed form during the 1-minor-release back-compat window)`);
|
|
168
|
+
}
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
144
171
|
const fileName = QA_EVIDENCE_FILE[gate.name];
|
|
145
172
|
const evidencePath = join(options.projectRoot, '.peaks', rdEvidenceDir, 'qa', fileName);
|
|
146
173
|
if (existsSync(evidencePath)) {
|
|
@@ -167,6 +194,22 @@ export async function verifyPipeline(options) {
|
|
|
167
194
|
const allQaGatesPassed = qaGates.every((g) => g.passed);
|
|
168
195
|
const complete = rdInvoked && qaInvoked && allRdGatesPassed && allQaGatesPassed
|
|
169
196
|
&& RD_QA_HANDOFF_STATES.has(rdState) && QA_COMPLETE_STATES.has(qaState);
|
|
197
|
+
// Slice 025 — derive the `acceptedForm` and `gateC` verdict. The form is
|
|
198
|
+
// 'suffixed' if both the security + perf gates passed via the new
|
|
199
|
+
// per-rid path; 'legacy' if either was consumed via the legacy fallback;
|
|
200
|
+
// 'none' if neither passed.
|
|
201
|
+
const secGate = qaGates.find((g) => g.name === 'security-findings');
|
|
202
|
+
const perfGate = qaGates.find((g) => g.name === 'performance-findings');
|
|
203
|
+
const secForm = secGate?.detail?.includes(`-${options.rid}.md`) ? 'suffixed' : 'legacy';
|
|
204
|
+
const perfForm = perfGate?.detail?.includes(`-${options.rid}.md`) ? 'suffixed' : 'legacy';
|
|
205
|
+
const acceptedForm = !secGate?.passed && !perfGate?.passed
|
|
206
|
+
? 'none'
|
|
207
|
+
: (secForm === 'suffixed' && perfForm === 'suffixed')
|
|
208
|
+
? 'suffixed'
|
|
209
|
+
: (secForm === 'legacy' || perfForm === 'legacy')
|
|
210
|
+
? 'legacy'
|
|
211
|
+
: 'suffixed';
|
|
212
|
+
const gateC = allQaGatesPassed ? 'pass' : 'fail';
|
|
170
213
|
return {
|
|
171
214
|
rid: options.rid,
|
|
172
215
|
changeId: resolvedChangeId,
|
|
@@ -175,6 +218,8 @@ export async function verifyPipeline(options) {
|
|
|
175
218
|
rdPhase: { invoked: rdInvoked, state: rdState, gates: rdGates },
|
|
176
219
|
qaPhase: { invoked: qaInvoked, state: qaState, gates: qaGates },
|
|
177
220
|
violations,
|
|
178
|
-
nextActions
|
|
221
|
+
nextActions,
|
|
222
|
+
acceptedForm,
|
|
223
|
+
gateC
|
|
179
224
|
};
|
|
180
225
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ResultEnvelope } from '../../shared/result.js';
|
|
2
|
+
export type PlanType = 'security' | 'perf';
|
|
3
|
+
/** Back-compat env-var. When set to "1", fall back to legacy paths. */
|
|
4
|
+
export declare const BACK_COMPAT_FLAG = "PEAKS_PLAN_LEGACY_FALLBACK";
|
|
5
|
+
/** F-1 (slice 025 security): canonical session-id shape. */
|
|
6
|
+
export declare const SESSION_ID_PATTERN: RegExp;
|
|
7
|
+
export interface ReadPlanArgs {
|
|
8
|
+
readonly type: PlanType;
|
|
9
|
+
readonly project: string;
|
|
10
|
+
readonly sessionId: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ReadPlanData {
|
|
13
|
+
readonly type: PlanType;
|
|
14
|
+
readonly exists: boolean;
|
|
15
|
+
readonly path: string;
|
|
16
|
+
readonly hash: string | null;
|
|
17
|
+
readonly refreshedAt: string | null;
|
|
18
|
+
/** `'canonical' | 'legacy' | 'missing'` — surfaced to the slice workflow
|
|
19
|
+
* so it can warn the user about a back-compat fallback. */
|
|
20
|
+
readonly source: 'canonical' | 'legacy' | 'missing';
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Normalize a markdown body for hashing. Sections sorted, blank lines
|
|
24
|
+
* collapsed, leading/trailing whitespace stripped. Hash is sha256[0:12].
|
|
25
|
+
*/
|
|
26
|
+
export declare function normalizePlanBody(body: string): string;
|
|
27
|
+
/** Compute the deterministic plan hash on a normalized body. */
|
|
28
|
+
export declare function hashNormalizedBody(body: string): string;
|
|
29
|
+
export declare function readPlan(args: ReadPlanArgs): ResultEnvelope<ReadPlanData>;
|