peaks-cli 1.0.11 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/bin/peaks.js +0 -0
  2. package/dist/src/cli/commands/core-artifact-commands.js +23 -0
  3. package/dist/src/cli/commands/mcp-commands.d.ts +3 -0
  4. package/dist/src/cli/commands/mcp-commands.js +144 -0
  5. package/dist/src/cli/commands/openspec-commands.d.ts +3 -0
  6. package/dist/src/cli/commands/openspec-commands.js +169 -0
  7. package/dist/src/cli/commands/project-commands.d.ts +3 -0
  8. package/dist/src/cli/commands/project-commands.js +37 -0
  9. package/dist/src/cli/commands/request-commands.d.ts +3 -0
  10. package/dist/src/cli/commands/request-commands.js +140 -0
  11. package/dist/src/cli/commands/understand-commands.d.ts +3 -0
  12. package/dist/src/cli/commands/understand-commands.js +78 -0
  13. package/dist/src/cli/program.js +10 -0
  14. package/dist/src/services/artifacts/request-artifact-service.d.ts +58 -0
  15. package/dist/src/services/artifacts/request-artifact-service.js +432 -0
  16. package/dist/src/services/dashboard/project-dashboard-service.d.ts +64 -0
  17. package/dist/src/services/dashboard/project-dashboard-service.js +112 -0
  18. package/dist/src/services/doctor/doctor-service.d.ts +7 -0
  19. package/dist/src/services/doctor/doctor-service.js +139 -0
  20. package/dist/src/services/mcp/mcp-apply-service.d.ts +31 -0
  21. package/dist/src/services/mcp/mcp-apply-service.js +112 -0
  22. package/dist/src/services/mcp/mcp-call-service.d.ts +17 -0
  23. package/dist/src/services/mcp/mcp-call-service.js +34 -0
  24. package/dist/src/services/mcp/mcp-client-service.d.ts +14 -0
  25. package/dist/src/services/mcp/mcp-client-service.js +49 -0
  26. package/dist/src/services/mcp/mcp-install-registry.d.ts +11 -0
  27. package/dist/src/services/mcp/mcp-install-registry.js +38 -0
  28. package/dist/src/services/mcp/mcp-plan-service.d.ts +29 -0
  29. package/dist/src/services/mcp/mcp-plan-service.js +109 -0
  30. package/dist/src/services/mcp/mcp-protocol.d.ts +24 -0
  31. package/dist/src/services/mcp/mcp-protocol.js +41 -0
  32. package/dist/src/services/mcp/mcp-scan-service.d.ts +8 -0
  33. package/dist/src/services/mcp/mcp-scan-service.js +214 -0
  34. package/dist/src/services/mcp/mcp-stdio-transport.d.ts +10 -0
  35. package/dist/src/services/mcp/mcp-stdio-transport.js +50 -0
  36. package/dist/src/services/mcp/mcp-types.d.ts +31 -0
  37. package/dist/src/services/mcp/mcp-types.js +1 -0
  38. package/dist/src/services/openspec/openspec-archive-service.d.ts +12 -0
  39. package/dist/src/services/openspec/openspec-archive-service.js +28 -0
  40. package/dist/src/services/openspec/openspec-bridge-service.d.ts +16 -0
  41. package/dist/src/services/openspec/openspec-bridge-service.js +76 -0
  42. package/dist/src/services/openspec/openspec-render-service.d.ts +38 -0
  43. package/dist/src/services/openspec/openspec-render-service.js +130 -0
  44. package/dist/src/services/openspec/openspec-scan-service.d.ts +6 -0
  45. package/dist/src/services/openspec/openspec-scan-service.js +123 -0
  46. package/dist/src/services/openspec/openspec-types.d.ts +39 -0
  47. package/dist/src/services/openspec/openspec-types.js +1 -0
  48. package/dist/src/services/openspec/openspec-validate-service.d.ts +27 -0
  49. package/dist/src/services/openspec/openspec-validate-service.js +77 -0
  50. package/dist/src/services/recommendations/capability-seed-items.js +1 -0
  51. package/dist/src/services/skills/skill-runbook-service.d.ts +11 -0
  52. package/dist/src/services/skills/skill-runbook-service.js +60 -0
  53. package/dist/src/services/standards/project-standards-service.js +4 -9
  54. package/dist/src/services/understand/understand-scan-service.d.ts +28 -0
  55. package/dist/src/services/understand/understand-scan-service.js +157 -0
  56. package/dist/src/services/understand/understand-types.d.ts +24 -0
  57. package/dist/src/services/understand/understand-types.js +1 -0
  58. package/dist/src/shared/json-schema-mini.d.ts +10 -0
  59. package/dist/src/shared/json-schema-mini.js +113 -0
  60. package/dist/src/shared/paths.d.ts +1 -1
  61. package/dist/src/shared/paths.js +9 -1
  62. package/dist/src/shared/version.d.ts +1 -1
  63. package/dist/src/shared/version.js +1 -1
  64. package/package.json +1 -6
  65. package/schemas/doctor-report.schema.json +34 -0
  66. package/schemas/mcp-apply-result.schema.json +46 -0
  67. package/schemas/mcp-install-plan.schema.json +71 -0
  68. package/schemas/mcp-install-spec.schema.json +29 -0
  69. package/schemas/mcp-server.schema.json +29 -0
  70. package/schemas/openspec-change-summary.schema.json +68 -0
  71. package/schemas/openspec-render-request.schema.json +61 -0
  72. package/schemas/openspec-validation-result.schema.json +36 -0
  73. package/skills/peaks-prd/SKILL.md +59 -8
  74. package/skills/peaks-prd/references/artifact-per-request.md +78 -0
  75. package/skills/peaks-prd/references/workflow.md +7 -5
  76. package/skills/peaks-qa/SKILL.md +73 -7
  77. package/skills/peaks-qa/references/artifact-contracts.md +1 -1
  78. package/skills/peaks-qa/references/artifact-per-request.md +83 -0
  79. package/skills/peaks-qa/references/openspec-validation-gate.md +55 -0
  80. package/skills/peaks-qa/references/regression-gates.md +1 -1
  81. package/skills/peaks-rd/SKILL.md +94 -7
  82. package/skills/peaks-rd/references/artifact-per-request.md +90 -0
  83. package/skills/peaks-rd/references/openspec-mcp-cli.md +65 -0
  84. package/skills/peaks-sc/SKILL.md +44 -0
  85. package/skills/peaks-sc/references/openspec-commit-boundaries.md +33 -0
  86. package/skills/peaks-solo/SKILL.md +87 -4
  87. package/skills/peaks-solo/references/browser-workflow.md +114 -0
  88. package/skills/peaks-solo/references/external-skill-invocation.md +70 -0
  89. package/skills/peaks-solo/references/openspec-mcp-workflow.md +53 -0
  90. package/skills/peaks-solo/references/workflow.md +1 -1
  91. package/skills/peaks-txt/SKILL.md +42 -0
  92. package/skills/peaks-ui/SKILL.md +57 -33
  93. package/skills/peaks-ui/references/artifact-per-request.md +71 -0
  94. package/skills/peaks-ui/references/workflow.md +8 -11
@@ -0,0 +1,123 @@
1
+ import { join } from 'node:path';
2
+ import { isDirectory, listDirectories, pathExists, readText } from '../../shared/fs.js';
3
+ function defaultOpenSpecRoot() {
4
+ return join(process.cwd(), 'openspec');
5
+ }
6
+ function parseMarkdownSections(markdown) {
7
+ const sections = new Map();
8
+ const lines = markdown.split(/\r?\n/);
9
+ let currentHeading = null;
10
+ let buffer = [];
11
+ const flush = () => {
12
+ if (currentHeading !== null) {
13
+ sections.set(currentHeading, buffer.join('\n').trim());
14
+ }
15
+ };
16
+ for (const line of lines) {
17
+ if (line.startsWith('## ')) {
18
+ flush();
19
+ currentHeading = line.slice(3).trim();
20
+ buffer = [];
21
+ }
22
+ else if (currentHeading !== null) {
23
+ buffer.push(line);
24
+ }
25
+ }
26
+ flush();
27
+ return sections;
28
+ }
29
+ function parseBullets(content) {
30
+ return content
31
+ .split(/\r?\n/)
32
+ .map((line) => line.trim())
33
+ .filter((line) => line.startsWith('- ') || line === '-')
34
+ .map((line) => line.replace(/^-\s*/, '').trim())
35
+ .filter((line) => line.length > 0 && !line.startsWith('[ ]') && !line.startsWith('[x]') && !line.startsWith('[X]'));
36
+ }
37
+ function parseProposal(markdown) {
38
+ const sections = parseMarkdownSections(markdown);
39
+ const why = sections.get('Why') ?? '';
40
+ return {
41
+ why,
42
+ whatChanges: parseBullets(sections.get('What Changes') ?? ''),
43
+ outOfScope: parseBullets(sections.get('Out of Scope') ?? ''),
44
+ dependencies: parseBullets(sections.get('Dependencies') ?? ''),
45
+ risks: parseBullets(sections.get('Risks') ?? ''),
46
+ acceptanceCriteria: parseBullets(sections.get('Acceptance Criteria') ?? '')
47
+ };
48
+ }
49
+ function parseTaskProgress(markdown) {
50
+ const sections = parseMarkdownSections(markdown);
51
+ const sectionEntries = [];
52
+ let totalTodo = 0;
53
+ let doneTodo = 0;
54
+ for (const [heading, body] of sections.entries()) {
55
+ let sectionTotal = 0;
56
+ let sectionDone = 0;
57
+ for (const line of body.split(/\r?\n/)) {
58
+ const trimmed = line.trim();
59
+ if (/^- \[[ xX]\]/.test(trimmed)) {
60
+ sectionTotal += 1;
61
+ if (/^- \[[xX]\]/.test(trimmed)) {
62
+ sectionDone += 1;
63
+ }
64
+ }
65
+ }
66
+ if (sectionTotal > 0) {
67
+ sectionEntries.push({ heading, total: sectionTotal, done: sectionDone });
68
+ totalTodo += sectionTotal;
69
+ doneTodo += sectionDone;
70
+ }
71
+ }
72
+ return { totalTodo, doneTodo, sections: sectionEntries };
73
+ }
74
+ async function listSpecs(changeRoot) {
75
+ const specsRoot = join(changeRoot, 'specs');
76
+ if (!(await isDirectory(specsRoot))) {
77
+ return [];
78
+ }
79
+ return listDirectories(specsRoot);
80
+ }
81
+ async function resolvePaths(changeRoot) {
82
+ const proposalPath = join(changeRoot, 'proposal.md');
83
+ const tasksPath = join(changeRoot, 'tasks.md');
84
+ const designPath = join(changeRoot, 'design.md');
85
+ return {
86
+ root: changeRoot,
87
+ proposal: (await pathExists(proposalPath)) ? proposalPath : null,
88
+ tasks: (await pathExists(tasksPath)) ? tasksPath : null,
89
+ design: (await pathExists(designPath)) ? designPath : null
90
+ };
91
+ }
92
+ async function buildSummary(id, changeRoot) {
93
+ const paths = await resolvePaths(changeRoot);
94
+ const specs = await listSpecs(changeRoot);
95
+ let taskProgress = null;
96
+ if (paths.tasks !== null) {
97
+ taskProgress = parseTaskProgress(await readText(paths.tasks));
98
+ }
99
+ return { id, paths, specs, taskProgress };
100
+ }
101
+ export async function scanOpenSpec(options = {}) {
102
+ const openspecRoot = options.openspecRoot ?? defaultOpenSpecRoot();
103
+ const changesRoot = join(openspecRoot, 'changes');
104
+ if (!(await isDirectory(openspecRoot))) {
105
+ return { openspecRoot, changesRoot, exists: false, changes: [] };
106
+ }
107
+ if (!(await isDirectory(changesRoot))) {
108
+ return { openspecRoot, changesRoot, exists: true, changes: [] };
109
+ }
110
+ const ids = await listDirectories(changesRoot);
111
+ const changes = await Promise.all(ids.map((id) => buildSummary(id, join(changesRoot, id))));
112
+ return { openspecRoot, changesRoot, exists: true, changes };
113
+ }
114
+ export async function loadOpenSpecChange(changeId, options = {}) {
115
+ const openspecRoot = options.openspecRoot ?? defaultOpenSpecRoot();
116
+ const changeRoot = join(openspecRoot, 'changes', changeId);
117
+ if (!(await isDirectory(changeRoot))) {
118
+ return null;
119
+ }
120
+ const summary = await buildSummary(changeId, changeRoot);
121
+ const proposal = summary.paths.proposal === null ? null : parseProposal(await readText(summary.paths.proposal));
122
+ return { ...summary, proposal };
123
+ }
@@ -0,0 +1,39 @@
1
+ export type OpenSpecProposal = {
2
+ why: string;
3
+ whatChanges: string[];
4
+ outOfScope: string[];
5
+ dependencies: string[];
6
+ risks: string[];
7
+ acceptanceCriteria: string[];
8
+ };
9
+ export type OpenSpecTaskSection = {
10
+ heading: string;
11
+ total: number;
12
+ done: number;
13
+ };
14
+ export type OpenSpecTaskProgress = {
15
+ totalTodo: number;
16
+ doneTodo: number;
17
+ sections: OpenSpecTaskSection[];
18
+ };
19
+ export type OpenSpecChangePaths = {
20
+ root: string;
21
+ proposal: string | null;
22
+ tasks: string | null;
23
+ design: string | null;
24
+ };
25
+ export type OpenSpecChangeSummary = {
26
+ id: string;
27
+ paths: OpenSpecChangePaths;
28
+ specs: string[];
29
+ taskProgress: OpenSpecTaskProgress | null;
30
+ };
31
+ export type OpenSpecChangeDetail = OpenSpecChangeSummary & {
32
+ proposal: OpenSpecProposal | null;
33
+ };
34
+ export type OpenSpecScanReport = {
35
+ openspecRoot: string;
36
+ changesRoot: string;
37
+ exists: boolean;
38
+ changes: OpenSpecChangeSummary[];
39
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import { type OpenSpecScanOptions } from './openspec-scan-service.js';
2
+ export type OpenSpecValidationLevel = 'error' | 'warning';
3
+ export type OpenSpecValidationIssue = {
4
+ level: OpenSpecValidationLevel;
5
+ rule: string;
6
+ message: string;
7
+ };
8
+ export type OpenSpecValidationSource = 'internal' | 'openspec-cli';
9
+ export type OpenSpecValidationResult = {
10
+ changeId: string;
11
+ valid: boolean;
12
+ source: OpenSpecValidationSource;
13
+ issues: OpenSpecValidationIssue[];
14
+ cliOutput?: string;
15
+ };
16
+ export type ExternalRunnerOutcome = {
17
+ available: boolean;
18
+ exitCode: number | null;
19
+ stdout: string;
20
+ stderr: string;
21
+ };
22
+ export type ExternalRunner = (command: string, args: string[]) => Promise<ExternalRunnerOutcome>;
23
+ export type OpenSpecValidateOptions = OpenSpecScanOptions & {
24
+ preferExternal?: boolean;
25
+ externalRunner?: ExternalRunner;
26
+ };
27
+ export declare function validateOpenSpecChange(changeId: string, options?: OpenSpecValidateOptions): Promise<OpenSpecValidationResult | null>;
@@ -0,0 +1,77 @@
1
+ import { join } from 'node:path';
2
+ import { isDirectory } from '../../shared/fs.js';
3
+ import { loadOpenSpecChange } from './openspec-scan-service.js';
4
+ const CHANGE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
5
+ function defaultOpenSpecRoot() {
6
+ return join(process.cwd(), 'openspec');
7
+ }
8
+ async function defaultExternalRunner(_command, _args) {
9
+ return { available: false, exitCode: null, stdout: '', stderr: '' };
10
+ }
11
+ function buildInternalIssues(changeId, detail) {
12
+ const issues = [];
13
+ if (!CHANGE_ID_PATTERN.test(changeId)) {
14
+ issues.push({
15
+ level: 'error',
16
+ rule: 'change-id-format',
17
+ message: `changeId ${changeId} does not match [A-Za-z0-9][A-Za-z0-9._-]*`
18
+ });
19
+ }
20
+ if (detail === null || detail.proposal === null) {
21
+ issues.push({ level: 'error', rule: 'proposal-exists', message: 'proposal.md is missing' });
22
+ return issues;
23
+ }
24
+ const proposal = detail.proposal;
25
+ if (proposal.why.length === 0) {
26
+ issues.push({ level: 'warning', rule: 'why-non-empty', message: 'Why section is empty' });
27
+ }
28
+ if (proposal.whatChanges.length === 0) {
29
+ issues.push({ level: 'error', rule: 'what-changes-non-empty', message: 'What Changes section has no bullets' });
30
+ }
31
+ if (proposal.acceptanceCriteria.length === 0) {
32
+ issues.push({ level: 'error', rule: 'acceptance-non-empty', message: 'Acceptance Criteria section has no bullets' });
33
+ }
34
+ return issues;
35
+ }
36
+ function hasErrors(issues) {
37
+ return issues.some((issue) => issue.level === 'error');
38
+ }
39
+ async function runInternal(changeId, openspecRoot) {
40
+ const changeRoot = join(openspecRoot, 'changes', changeId);
41
+ if (!(await isDirectory(changeRoot))) {
42
+ return null;
43
+ }
44
+ const detail = await loadOpenSpecChange(changeId, { openspecRoot });
45
+ const issues = buildInternalIssues(changeId, detail);
46
+ return {
47
+ changeId,
48
+ valid: !hasErrors(issues),
49
+ source: 'internal',
50
+ issues
51
+ };
52
+ }
53
+ export async function validateOpenSpecChange(changeId, options = {}) {
54
+ const openspecRoot = options.openspecRoot ?? defaultOpenSpecRoot();
55
+ const runner = options.externalRunner ?? defaultExternalRunner;
56
+ if (options.preferExternal === true) {
57
+ const outcome = await runner('openspec', ['validate', changeId]);
58
+ if (outcome.available) {
59
+ const cliOutput = [outcome.stdout, outcome.stderr].filter((part) => part.length > 0).join('\n').trim();
60
+ const passed = outcome.exitCode === 0;
61
+ const issues = passed
62
+ ? []
63
+ : [{ level: 'error', rule: 'openspec-cli-failed', message: `openspec validate exited with code ${outcome.exitCode ?? 'null'}` }];
64
+ return { changeId, valid: passed, source: 'openspec-cli', issues, cliOutput };
65
+ }
66
+ const internal = await runInternal(changeId, openspecRoot);
67
+ if (internal === null) {
68
+ return null;
69
+ }
70
+ internal.issues = [
71
+ { level: 'warning', rule: 'openspec-cli-unavailable', message: 'openspec CLI not found, fell back to internal lint' },
72
+ ...internal.issues
73
+ ];
74
+ return internal;
75
+ }
76
+ return runInternal(changeId, openspecRoot);
77
+ }
@@ -100,6 +100,7 @@ export const seedCapabilityItems = [
100
100
  capability('gitnexus.repo-intelligence', 'gitnexus', 'GitNexus Repository Intelligence', 'cli', 'repo-intelligence', ['engineer'], 'medium', 'local-repo-scan', 'Use local project scanning through Peaks RD.', 'Repository Intelligence', '仓库智能分析', 'Repository intelligence should be proxied through Peaks before use.', '仓库智能分析应先通过 Peaks 代理边界再使用。'),
101
101
  capability('claude-code-best-practice.workflow-guidance', 'claude-code-best-practice', 'Claude Code Best Practice', 'doc', 'workflow-guidance', ['engineer'], 'low', 'peaks-built-in-rules', 'Use Peaks built-in workflow and review rules.', 'Claude Code Best Practice', 'Claude Code 最佳实践', 'Guidance for Claude Code engineering workflows.', 'Claude Code 工程工作流指导。'),
102
102
  capability('superpowers.workflow-methodology', 'superpowers', 'Superpowers Methodology', 'workflow', 'workflow-methodology', ['product', 'engineer'], 'low', 'peaks-workflow-route', 'Use Peaks route/autonomous plans.', 'Superpowers Methodology', 'Superpowers 方法论', 'Workflow and artifact methodology reference.', '工作流与 artifact 方法论参考。'),
103
+ capability('understand-anything.knowledge-graph', 'understand-anything', 'Understand Anything Knowledge Graph', 'skill', 'project-analysis', ['engineer', 'product', 'qa'], 'low', 'peaks-codegraph-or-rd-scan', 'Use peaks codegraph or peaks RD project scan when Understand Anything is not installed. Optionally suggest installing it via Claude Code: /plugin marketplace add Lum1104/Understand-Anything then /plugin install understand-anything.', 'Understand Anything Knowledge Graph', 'Understand Anything 知识图谱', 'Multi-agent project knowledge graph that Peaks reads from .understand-anything/knowledge-graph.json when it has been generated by the Understand Anything plugin in Claude Code.', '通过 Claude Code 的 Understand Anything 插件生成的多 agent 知识图谱,Peaks 在 .understand-anything/knowledge-graph.json 存在时读取它作为项目分析证据。'),
103
104
  capability('penpot.design-source', 'penpot', 'Penpot Design Source', 'doc', 'design-source', ['designer', 'engineer'], 'medium', 'manual-design-input', 'Ask for exported design notes or screenshots.', 'Penpot Design Source', 'Penpot 设计来源', 'Design-source reference for UI planning.', '用于 UI 规划的设计来源参考。'),
104
105
  capability('gstack.product-stack-guidance', 'gstack', 'Product Stack Guidance', 'doc', 'product-guidance', ['product'], 'low', 'peaks-prd', 'Use peaks-prd for product goals and non-goals.', 'Product Stack Guidance', '产品栈指导', 'Product and startup stack guidance reference.', '产品与创业栈指导参考。'),
105
106
  capability('awesome-design-md.design-reference', 'awesome-design-md', 'Design Markdown Reference', 'doc', 'design-reference', ['designer', 'engineer'], 'low', 'peaks-ui-visual-direction', 'Use peaks-ui to define visual direction and interaction constraints.', 'Design Reference', '设计参考', 'Design markdown examples and inspiration.', '设计 markdown 示例与灵感。'),
@@ -0,0 +1,11 @@
1
+ export type SkillRunbookInspection = {
2
+ name: string;
3
+ directory: string;
4
+ hasRunbook: boolean;
5
+ peaksCommandCount: number;
6
+ peaksCommandLines: string[];
7
+ destructiveApplyLines: string[];
8
+ hasAuthorizationNote: boolean;
9
+ ok: boolean;
10
+ };
11
+ export declare function inspectSkillRunbook(name: string, baseDir?: string): Promise<SkillRunbookInspection>;
@@ -0,0 +1,60 @@
1
+ import { readText } from '../../shared/fs.js';
2
+ import { loadSkillRegistry } from './skill-registry.js';
3
+ const DESTRUCTIVE_APPLY_PATTERNS = [
4
+ /peaks\s+memory\s+sync[^\n]*--apply/,
5
+ /peaks\s+memory\s+extract[^\n]*--apply/,
6
+ /peaks\s+artifacts\s+sync[^\n]*--apply/,
7
+ /peaks\s+openspec\s+archive[^\n]*--apply/,
8
+ /peaks\s+standards\s+(?:init|update)[^\n]*--apply/
9
+ ];
10
+ const AUTHORIZATION_KEYWORDS_PATTERN = /authoriz|explicit|--dry-run|approv|only after|only when/i;
11
+ const PEAKS_COMMAND_LINE_PATTERN = /^\s*peaks\s+\w/;
12
+ function extractRunbookSection(body) {
13
+ const match = /## Default runbook\n+([\s\S]*?)(?=\n## |$)/.exec(body);
14
+ return match === null ? null : (match[1] ?? null);
15
+ }
16
+ function findDestructiveApplyLines(section) {
17
+ const lines = section.split(/\r?\n/);
18
+ return lines.filter((line) => DESTRUCTIVE_APPLY_PATTERNS.some((pattern) => pattern.test(line)));
19
+ }
20
+ function findPeaksCommandLines(section) {
21
+ return section
22
+ .split(/\r?\n/)
23
+ .filter((line) => PEAKS_COMMAND_LINE_PATTERN.test(line))
24
+ .map((line) => line.trim());
25
+ }
26
+ export async function inspectSkillRunbook(name, baseDir) {
27
+ const registry = await loadSkillRegistry(baseDir);
28
+ const skill = registry.skills.find((entry) => entry.name === name);
29
+ if (skill === undefined) {
30
+ throw new Error(`Skill "${name}" not found under skills directory`);
31
+ }
32
+ const body = await readText(skill.skillPath);
33
+ const section = extractRunbookSection(body);
34
+ if (section === null) {
35
+ return {
36
+ name: skill.name,
37
+ directory: skill.directory,
38
+ hasRunbook: false,
39
+ peaksCommandCount: 0,
40
+ peaksCommandLines: [],
41
+ destructiveApplyLines: [],
42
+ hasAuthorizationNote: false,
43
+ ok: false
44
+ };
45
+ }
46
+ const peaksCommandLines = findPeaksCommandLines(section);
47
+ const destructiveApplyLines = findDestructiveApplyLines(section);
48
+ const hasAuthorizationNote = AUTHORIZATION_KEYWORDS_PATTERN.test(section);
49
+ const ok = destructiveApplyLines.length === 0 || hasAuthorizationNote;
50
+ return {
51
+ name: skill.name,
52
+ directory: skill.directory,
53
+ hasRunbook: true,
54
+ peaksCommandCount: peaksCommandLines.length,
55
+ peaksCommandLines,
56
+ destructiveApplyLines,
57
+ hasAuthorizationNote,
58
+ ok
59
+ };
60
+ }
@@ -96,20 +96,17 @@ function renderClaudeMd(language) {
96
96
  '- peaks-solo summarizes RD and QA standards preflight before end-to-end code workflows.',
97
97
  '',
98
98
  'Rules:',
99
- '- RED LINE: Read `.claude/rules/common/coding-style.md` before editing code, and treat its file-size limits as blockers.',
100
- '- RED LINE: Strictly prohibit oversized single files; split large files into cohesive modules before handoff.',
99
+ '- Read `.claude/rules/common/coding-style.md` before editing code.',
101
100
  '- Read `.claude/rules/common/code-review.md` before reviewing changes.',
102
101
  '- Read `.claude/rules/common/security.md` before touching filesystem, user input, external calls, auth, or secrets.',
103
102
  `- Read .claude/rules/${language}/coding-style.md for language-specific standards when applicable.`,
104
103
  '',
105
- 'External references: https://github.com/affaan-m/everything-claude-code and https://github.com/SquabbyZ/andrej-karpathy-skills are used as curated references only. Treat andrej-karpathy-skills code quality guidance as a red line during peaks-rd implementation. Do not execute or install external content without explicit approval.',
104
+ '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.',
106
105
  ''
107
106
  ].join('\n');
108
107
  }
109
108
  function renderCommonCodingStyle() {
110
- return `${renderHeader('Common Coding Standards')}- RED LINE: Strictly prohibit oversized single files; split large files into cohesive modules before handoff.
111
- - RED LINE: Treat https://github.com/SquabbyZ/andrej-karpathy-skills code quality guidance as a blocker for peaks-rd implementation.
112
- - Prefer simple, readable code over clever abstractions.
109
+ return `${renderHeader('Common Coding Standards')}- Prefer simple, readable code over clever abstractions.
113
110
  - Keep functions focused and files cohesive.
114
111
  - Use immutable updates unless a language-specific convention explicitly favors mutation.
115
112
  - Validate user input, external data, file paths, and configuration at system boundaries.
@@ -117,9 +114,7 @@ function renderCommonCodingStyle() {
117
114
  `;
118
115
  }
119
116
  function renderCodeReview() {
120
- return `${renderHeader('Code Review Standards')}- RED LINE: Block oversized single-file changes and require cohesive module splits before approval.
121
- - RED LINE: Treat https://github.com/SquabbyZ/andrej-karpathy-skills code quality guidance as a blocker during review.
122
- - Review diffs for correctness, maintainability, test coverage, and regression risk.
117
+ return `${renderHeader('Code Review Standards')}- Review diffs for correctness, maintainability, test coverage, and regression risk.
123
118
  - Treat missing tests for changed behavior as a blocker unless the change is documentation-only.
124
119
  - Verify code paths that handle filesystem, external APIs, credentials, user input, or generated artifacts.
125
120
  - peaks-qa must use this guidance as part of code workflow preflight and final verification.
@@ -0,0 +1,28 @@
1
+ import type { UnderstandScanReport } from './understand-types.js';
2
+ export type UnderstandScanOptions = {
3
+ projectRoot: string;
4
+ artifactDir?: string;
5
+ };
6
+ export declare function scanUnderstandAnything(options: UnderstandScanOptions): Promise<UnderstandScanReport>;
7
+ export type UnderstandGraphSummary = {
8
+ exists: boolean;
9
+ path: string;
10
+ generatedAt: string | null;
11
+ topLevelFields: string[];
12
+ counts: {
13
+ nodes: number;
14
+ edges: number;
15
+ layers: number;
16
+ tours: number;
17
+ };
18
+ layerNames: string[];
19
+ tourNames: string[];
20
+ sampleNodes: string[];
21
+ parseError?: string;
22
+ };
23
+ export type SummarizeKnowledgeGraphOptions = {
24
+ projectRoot: string;
25
+ artifactDir?: string;
26
+ sampleSize?: number;
27
+ };
28
+ export declare function summarizeKnowledgeGraph(options: SummarizeKnowledgeGraphOptions): Promise<UnderstandGraphSummary>;
@@ -0,0 +1,157 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { isDirectory, pathExists, readText } from '../../shared/fs.js';
4
+ import { getErrorMessage } from '../../shared/result.js';
5
+ function defaultArtifactDir(projectRoot) {
6
+ return join(projectRoot, '.understand-anything');
7
+ }
8
+ function countArrayField(record, key) {
9
+ const value = record[key];
10
+ return Array.isArray(value) ? value.length : 0;
11
+ }
12
+ async function readFlag(path) {
13
+ return { exists: await pathExists(path), path };
14
+ }
15
+ async function readGraph(graphPath) {
16
+ if (!(await pathExists(graphPath))) {
17
+ return { exists: false, path: graphPath };
18
+ }
19
+ const stats = await stat(graphPath);
20
+ const raw = await readText(graphPath);
21
+ try {
22
+ const parsed = JSON.parse(raw);
23
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
24
+ return {
25
+ exists: true,
26
+ path: graphPath,
27
+ sizeBytes: stats.size,
28
+ parseError: 'knowledge-graph.json must be a JSON object'
29
+ };
30
+ }
31
+ const record = parsed;
32
+ return {
33
+ exists: true,
34
+ path: graphPath,
35
+ sizeBytes: stats.size,
36
+ topLevelFields: Object.keys(record).sort(),
37
+ counts: {
38
+ nodes: countArrayField(record, 'nodes'),
39
+ edges: countArrayField(record, 'edges'),
40
+ layers: countArrayField(record, 'layers'),
41
+ tours: countArrayField(record, 'tours')
42
+ }
43
+ };
44
+ }
45
+ catch (error) {
46
+ return {
47
+ exists: true,
48
+ path: graphPath,
49
+ sizeBytes: stats.size,
50
+ parseError: getErrorMessage(error)
51
+ };
52
+ }
53
+ }
54
+ export async function scanUnderstandAnything(options) {
55
+ const artifactDir = options.artifactDir ?? defaultArtifactDir(options.projectRoot);
56
+ const exists = await isDirectory(artifactDir);
57
+ if (!exists) {
58
+ return {
59
+ exists: false,
60
+ artifactDir,
61
+ graph: { exists: false, path: join(artifactDir, 'knowledge-graph.json') },
62
+ intermediate: { exists: false, path: join(artifactDir, 'intermediate') },
63
+ diffOverlay: { exists: false, path: join(artifactDir, 'diff-overlay.json') }
64
+ };
65
+ }
66
+ const graph = await readGraph(join(artifactDir, 'knowledge-graph.json'));
67
+ const intermediate = await readFlag(join(artifactDir, 'intermediate'));
68
+ const diffOverlay = await readFlag(join(artifactDir, 'diff-overlay.json'));
69
+ return { exists: true, artifactDir, graph, intermediate, diffOverlay };
70
+ }
71
+ function pickStringId(value) {
72
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
73
+ return null;
74
+ }
75
+ const record = value;
76
+ for (const key of ['id', 'path', 'name', 'label']) {
77
+ const candidate = record[key];
78
+ if (typeof candidate === 'string' && candidate.length > 0) {
79
+ return candidate;
80
+ }
81
+ }
82
+ return null;
83
+ }
84
+ function pickStringField(record, key) {
85
+ const value = record[key];
86
+ return typeof value === 'string' && value.length > 0 ? value : null;
87
+ }
88
+ function pickNameArray(record, key) {
89
+ const value = record[key];
90
+ if (!Array.isArray(value)) {
91
+ return [];
92
+ }
93
+ return value
94
+ .map((entry) => {
95
+ if (typeof entry === 'string') {
96
+ return entry;
97
+ }
98
+ return pickStringId(entry);
99
+ })
100
+ .filter((entry) => entry !== null);
101
+ }
102
+ export async function summarizeKnowledgeGraph(options) {
103
+ const scanOptions = { projectRoot: options.projectRoot };
104
+ if (options.artifactDir !== undefined) {
105
+ scanOptions.artifactDir = options.artifactDir;
106
+ }
107
+ const scan = await scanUnderstandAnything(scanOptions);
108
+ const sampleSize = options.sampleSize ?? 5;
109
+ if (!scan.graph.exists) {
110
+ return {
111
+ exists: false,
112
+ path: scan.graph.path,
113
+ generatedAt: null,
114
+ topLevelFields: [],
115
+ counts: { nodes: 0, edges: 0, layers: 0, tours: 0 },
116
+ layerNames: [],
117
+ tourNames: [],
118
+ sampleNodes: []
119
+ };
120
+ }
121
+ if (scan.graph.parseError !== undefined) {
122
+ return {
123
+ exists: true,
124
+ path: scan.graph.path,
125
+ generatedAt: null,
126
+ topLevelFields: [],
127
+ counts: { nodes: 0, edges: 0, layers: 0, tours: 0 },
128
+ layerNames: [],
129
+ tourNames: [],
130
+ sampleNodes: [],
131
+ parseError: scan.graph.parseError
132
+ };
133
+ }
134
+ const raw = await readText(scan.graph.path);
135
+ const parsed = JSON.parse(raw);
136
+ const record = parsed;
137
+ const generatedAt = pickStringField(record, 'generatedAt');
138
+ const nodes = Array.isArray(record.nodes) ? record.nodes : [];
139
+ const sampleNodes = nodes.slice(0, sampleSize)
140
+ .map((entry) => pickStringId(entry))
141
+ .filter((entry) => entry !== null);
142
+ return {
143
+ exists: true,
144
+ path: scan.graph.path,
145
+ generatedAt,
146
+ topLevelFields: Object.keys(record).sort(),
147
+ counts: {
148
+ nodes: countArrayField(record, 'nodes'),
149
+ edges: countArrayField(record, 'edges'),
150
+ layers: countArrayField(record, 'layers'),
151
+ tours: countArrayField(record, 'tours')
152
+ },
153
+ layerNames: pickNameArray(record, 'layers'),
154
+ tourNames: pickNameArray(record, 'tours'),
155
+ sampleNodes
156
+ };
157
+ }
@@ -0,0 +1,24 @@
1
+ export type UnderstandGraphReport = {
2
+ exists: boolean;
3
+ path: string;
4
+ sizeBytes?: number;
5
+ topLevelFields?: string[];
6
+ counts?: {
7
+ nodes: number;
8
+ edges: number;
9
+ layers: number;
10
+ tours: number;
11
+ };
12
+ parseError?: string;
13
+ };
14
+ export type UnderstandFlagReport = {
15
+ exists: boolean;
16
+ path: string;
17
+ };
18
+ export type UnderstandScanReport = {
19
+ exists: boolean;
20
+ artifactDir: string;
21
+ graph: UnderstandGraphReport;
22
+ intermediate: UnderstandFlagReport;
23
+ diffOverlay: UnderstandFlagReport;
24
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ export type JsonSchemaIssue = {
2
+ path: string;
3
+ message: string;
4
+ };
5
+ export type JsonSchemaResult = {
6
+ valid: boolean;
7
+ errors: JsonSchemaIssue[];
8
+ };
9
+ export type JsonSchemaNode = Record<string, unknown>;
10
+ export declare function validateAgainstSchema(value: unknown, schema: JsonSchemaNode): JsonSchemaResult;