peaks-cli 1.2.8 → 1.2.9

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 (31) hide show
  1. package/README.md +12 -0
  2. package/bin/peaks.js +0 -0
  3. package/dist/src/cli/commands/project-commands.js +1 -1
  4. package/dist/src/cli/commands/scan-commands.js +22 -0
  5. package/dist/src/services/memory/project-memory-service.d.ts +1 -1
  6. package/dist/src/services/memory/project-memory-service.js +52 -23
  7. package/dist/src/services/scan/libraries-service.d.ts +24 -0
  8. package/dist/src/services/scan/libraries-service.js +419 -0
  9. package/dist/src/services/scan/libraries-types.d.ts +59 -0
  10. package/dist/src/services/scan/libraries-types.js +9 -0
  11. package/dist/src/services/skills/skill-runbook-service.js +34 -1
  12. package/dist/src/services/workflow/autonomous-resume-writer.js +7 -7
  13. package/dist/src/shared/change-id.d.ts +30 -0
  14. package/dist/src/shared/change-id.js +40 -6
  15. package/dist/src/shared/paths.d.ts +1 -1
  16. package/dist/src/shared/paths.js +2 -1
  17. package/dist/src/shared/version.d.ts +1 -1
  18. package/dist/src/shared/version.js +1 -1
  19. package/package.json +1 -1
  20. package/schemas/library-breaking-changes.data.json +141 -0
  21. package/schemas/library-breaking-changes.meta.json +6 -0
  22. package/schemas/library-breaking-changes.schema.json +50 -0
  23. package/skills/peaks-qa/SKILL.md +12 -0
  24. package/skills/peaks-rd/SKILL.md +145 -2
  25. package/skills/peaks-solo/SKILL.md +76 -316
  26. package/skills/peaks-solo/references/runbook.md +166 -0
  27. package/skills/peaks-solo/references/workflow-gates-and-types.md +177 -0
  28. package/skills/peaks-solo-resume/SKILL.md +81 -0
  29. package/skills/peaks-solo-status/SKILL.md +120 -0
  30. package/skills/peaks-solo-test/SKILL.md +84 -0
  31. package/skills/peaks-txt/SKILL.md +8 -5
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Library version scan types.
3
+ * Joins the existing scan family (archetype, existing-system, type-sanity,
4
+ * acceptance-coverage, diff-vs-scope, file-size) but is intentionally scoped
5
+ * to library version enumeration only. Does NOT extend scan-types.ts —
6
+ * each scan service ships its own types to keep the scan-types module
7
+ * focused on the archetype + existing-system pair (see src/services/scan/scan-types.ts).
8
+ */
9
+ export type Ecosystem = 'npm';
10
+ export type DependencyScope = 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies';
11
+ export type LibraryEntry = {
12
+ /** npm package name (e.g. "antd", "@mui/material", "react-router-dom"). */
13
+ name: string;
14
+ /** Raw version spec as written in package.json (e.g. "^5.18.0", "workspace:*", "git+https://..."). */
15
+ version: string;
16
+ /**
17
+ * Parsed major version, or null when the spec is non-semver (e.g.
18
+ * "workspace:*", "file:../local", "git+https://..."). The LLM should
19
+ * treat null as "cannot determine major; consult breaking-changes table
20
+ * by other signals (e.g. lockfile or import statement shape)".
21
+ */
22
+ major: number | null;
23
+ scope: DependencyScope;
24
+ ecosystem: Ecosystem;
25
+ };
26
+ /**
27
+ * Per-workspace provenance for monorepo scans.
28
+ *
29
+ * `path` is the absolute path of the `package.json` that contributed
30
+ * libraries. `count` is the number of `LibraryEntry` rows produced by
31
+ * reading that single `package.json` (i.e. NOT the aggregate across
32
+ * the whole monorepo — use `LibraryReport.totalCount` for the aggregate).
33
+ *
34
+ * `name` and `version` are the workspace's own `name` / `version` from
35
+ * its `package.json`, when present. They are optional because some
36
+ * workspace `package.json` files omit them.
37
+ */
38
+ export type WorkspaceEntry = {
39
+ path: string;
40
+ count: number;
41
+ name?: string;
42
+ version?: string;
43
+ };
44
+ export type LibraryReport = {
45
+ projectRoot: string;
46
+ libraries: LibraryEntry[];
47
+ totalCount: number;
48
+ byScope: Record<DependencyScope, number>;
49
+ /**
50
+ * Per-workspace provenance for monorepo (pnpm / npm / yarn workspaces,
51
+ * lerna) projects. Empty for single-package projects so the field is
52
+ * always present (additive; consumers can rely on the shape).
53
+ */
54
+ workspaces: WorkspaceEntry[];
55
+ /** ISO timestamp at scan time. */
56
+ scannedAt: string;
57
+ /** Soft signals — e.g. "package.json not found" or "package.json is not valid JSON". */
58
+ warnings: string[];
59
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Library version scan types.
3
+ * Joins the existing scan family (archetype, existing-system, type-sanity,
4
+ * acceptance-coverage, diff-vs-scope, file-size) but is intentionally scoped
5
+ * to library version enumeration only. Does NOT extend scan-types.ts —
6
+ * each scan service ships its own types to keep the scan-types module
7
+ * focused on the archetype + existing-system pair (see src/services/scan/scan-types.ts).
8
+ */
9
+ export {};
@@ -1,3 +1,4 @@
1
+ import { dirname, join } from 'node:path';
1
2
  import { readText } from '../../shared/fs.js';
2
3
  import { loadSkillRegistry } from './skill-registry.js';
3
4
  const DESTRUCTIVE_APPLY_PATTERNS = [
@@ -13,6 +14,38 @@ function extractRunbookSection(body) {
13
14
  const match = /## Default runbook\n+([\s\S]*?)(?=\n## |$)/.exec(body);
14
15
  return match === null ? null : match[1];
15
16
  }
17
+ /**
18
+ * Load the runbook section, falling back to `references/runbook.md` if the
19
+ * SKILL.md only has a pointer section. This supports skills (notably
20
+ * `peaks-solo`) that extracted their 150-line bash runbook to a sibling
21
+ * reference to keep SKILL.md under the 800-line cap. The CLI
22
+ * `peaks skill runbook` command uses the same fallback so a human
23
+ * reviewer sees the full runbook regardless of where it lives.
24
+ *
25
+ * Strategy: prefer the LONGER of the two sections. A short pointer section
26
+ * in SKILL.md (~ 1-2 lines) is treated as a "this runbook is in the
27
+ * reference" marker; a long inline section (>= the reference length) is
28
+ * treated as the canonical runbook. This avoids the false positive where
29
+ * the pointer section's regex match returns a non-null but content-poor
30
+ * string.
31
+ */
32
+ async function loadRunbookSection(skillPath, body) {
33
+ const inline = extractRunbookSection(body);
34
+ const refPath = join(dirname(skillPath), 'references', 'runbook.md');
35
+ let refSection = null;
36
+ try {
37
+ const refBody = await readText(refPath);
38
+ refSection = extractRunbookSection(refBody);
39
+ }
40
+ catch {
41
+ // reference file does not exist or is not readable
42
+ }
43
+ if (inline === null)
44
+ return refSection;
45
+ if (refSection === null)
46
+ return inline;
47
+ return inline.length >= refSection.length ? inline : refSection;
48
+ }
16
49
  function findDestructiveApplyLines(section) {
17
50
  const lines = section.split(/\r?\n/);
18
51
  return lines.filter((line) => DESTRUCTIVE_APPLY_PATTERNS.some((pattern) => pattern.test(line)));
@@ -30,7 +63,7 @@ export async function inspectSkillRunbook(name, baseDir) {
30
63
  throw new Error(`Skill "${name}" not found under skills directory`);
31
64
  }
32
65
  const body = await readText(skill.skillPath);
33
- const section = extractRunbookSection(body);
66
+ const section = await loadRunbookSection(skill.skillPath, body);
34
67
  if (section === null) {
35
68
  return {
36
69
  name: skill.name,
@@ -1,6 +1,6 @@
1
1
  import { mkdir, writeFile } from 'node:fs/promises';
2
2
  import { dirname, join } from 'node:path';
3
- import { buildArtifactRelativePath, validateChangeIdOrThrow } from '../../shared/change-id.js';
3
+ import { buildArtifactRelativePathInRoot, validateChangeIdOrThrow } from '../../shared/change-id.js';
4
4
  import { pathExists } from '../../shared/fs.js';
5
5
  function defaultClock() {
6
6
  return new Date().toISOString();
@@ -109,27 +109,27 @@ Next actions:
109
109
  function buildFiles(changeId, goal, createdAt, artifactWorkspacePath) {
110
110
  return [
111
111
  {
112
- path: join(artifactWorkspacePath, buildArtifactRelativePath(changeId, 'prd', 'autonomous-goal-package.json')),
112
+ path: join(artifactWorkspacePath, buildArtifactRelativePathInRoot(artifactWorkspacePath, changeId, 'prd', 'autonomous-goal-package.json')),
113
113
  content: renderGoalPackage(changeId, goal)
114
114
  },
115
115
  {
116
- path: join(artifactWorkspacePath, buildArtifactRelativePath(changeId, 'rd', 'swarm', 'autonomous-rd-plan.json')),
116
+ path: join(artifactWorkspacePath, buildArtifactRelativePathInRoot(artifactWorkspacePath, changeId, 'rd', 'swarm', 'autonomous-rd-plan.json')),
117
117
  content: renderRdPlan(changeId)
118
118
  },
119
119
  {
120
- path: join(artifactWorkspacePath, buildArtifactRelativePath(changeId, 'rd', 'swarm', 'checkpoints', 'checkpoint-1.json')),
120
+ path: join(artifactWorkspacePath, buildArtifactRelativePathInRoot(artifactWorkspacePath, changeId, 'rd', 'swarm', 'checkpoints', 'checkpoint-1.json')),
121
121
  content: renderCheckpoint(changeId, createdAt)
122
122
  },
123
123
  {
124
- path: join(artifactWorkspacePath, buildArtifactRelativePath(changeId, 'rd', 'swarm', 'evidence', 'unit-tests.md')),
124
+ path: join(artifactWorkspacePath, buildArtifactRelativePathInRoot(artifactWorkspacePath, changeId, 'rd', 'swarm', 'evidence', 'unit-tests.md')),
125
125
  content: renderUnitTestsEvidence(changeId)
126
126
  },
127
127
  {
128
- path: join(artifactWorkspacePath, buildArtifactRelativePath(changeId, 'rd', 'swarm', 'evidence', 'validation-report.md')),
128
+ path: join(artifactWorkspacePath, buildArtifactRelativePathInRoot(artifactWorkspacePath, changeId, 'rd', 'swarm', 'evidence', 'validation-report.md')),
129
129
  content: renderValidationReport(changeId)
130
130
  },
131
131
  {
132
- path: join(artifactWorkspacePath, buildArtifactRelativePath(changeId, 'rd', 'swarm', 'resume-instructions.md')),
132
+ path: join(artifactWorkspacePath, buildArtifactRelativePathInRoot(artifactWorkspacePath, changeId, 'rd', 'swarm', 'resume-instructions.md')),
133
133
  content: renderResumeInstructions(changeId)
134
134
  }
135
135
  ];
@@ -11,6 +11,30 @@ export declare class ChangeIdValidationError extends Error {
11
11
  constructor(changeId: string);
12
12
  }
13
13
  export declare function isUnsafeArtifactPath(path: string): boolean;
14
+ /**
15
+ * Build an artifact-relative path using a caller-supplied project root, so
16
+ * the helper does not need to walk `process.cwd()` to find a session.
17
+ *
18
+ * If a session exists for `projectRoot`, files are stored in:
19
+ * .peaks/<sessionId>/<role>/<number>-<changeId>.md
20
+ *
21
+ * If no session exists, falls back to legacy behavior:
22
+ * .peaks/<changeId>/<segments>
23
+ *
24
+ * Use this from callers that have a workspace or `artifactWorkspacePath` in
25
+ * hand (e.g. CLI subcommands that received `--project`, or test fixtures
26
+ * that created a tmpdir workspace). Legacy callers without an explicit
27
+ * `projectRoot` should continue to use `buildArtifactRelativePath`.
28
+ *
29
+ * @param projectRoot - The project root to use for session lookup and dirPath
30
+ * computation. Must be an absolute path. Falls back to `process.cwd()` if
31
+ * the empty string is passed (defensive only; should not happen via the
32
+ * public API).
33
+ * @param changeId - Used as file description/slug (e.g., "auth-system", "add-user-auth")
34
+ * @param segments - Optional path segments (first segment is typically the role: 'prd', 'rd', 'qa', etc.)
35
+ * @returns Relative path to the artifact file
36
+ */
37
+ export declare function buildArtifactRelativePathInRoot(projectRoot: string, changeId: string, ...segments: string[]): string;
14
38
  /**
15
39
  * Build an artifact-relative path using session-based storage.
16
40
  *
@@ -20,6 +44,12 @@ export declare function isUnsafeArtifactPath(path: string): boolean;
20
44
  * If no session exists, falls back to legacy behavior:
21
45
  * .peaks/<changeId>/<segments>
22
46
  *
47
+ * This function walks `process.cwd()` to find the project root and reads
48
+ * `.peaks/.session.json` from it. Callers that already have an explicit
49
+ * `projectRoot` (workspace handle, test fixture, or CLI `--project` flag)
50
+ * should prefer `buildArtifactRelativePathInRoot(projectRoot, ...)` to
51
+ * avoid being polluted by the host environment's session binding.
52
+ *
23
53
  * @param changeId - Used as file description/slug (e.g., "auth-system", "add-user-auth")
24
54
  * @param segments - Optional path segments (first segment is typically the role: 'prd', 'rd', 'qa', etc.)
25
55
  * @returns Relative path to the artifact file
@@ -62,25 +62,37 @@ export function isUnsafeArtifactPath(path) {
62
62
  return isUnsafePathInput(path);
63
63
  }
64
64
  /**
65
- * Build an artifact-relative path using session-based storage.
65
+ * Build an artifact-relative path using a caller-supplied project root, so
66
+ * the helper does not need to walk `process.cwd()` to find a session.
66
67
  *
67
- * If a session exists, files are stored in:
68
+ * If a session exists for `projectRoot`, files are stored in:
68
69
  * .peaks/<sessionId>/<role>/<number>-<changeId>.md
69
70
  *
70
71
  * If no session exists, falls back to legacy behavior:
71
72
  * .peaks/<changeId>/<segments>
72
73
  *
74
+ * Use this from callers that have a workspace or `artifactWorkspacePath` in
75
+ * hand (e.g. CLI subcommands that received `--project`, or test fixtures
76
+ * that created a tmpdir workspace). Legacy callers without an explicit
77
+ * `projectRoot` should continue to use `buildArtifactRelativePath`.
78
+ *
79
+ * @param projectRoot - The project root to use for session lookup and dirPath
80
+ * computation. Must be an absolute path. Falls back to `process.cwd()` if
81
+ * the empty string is passed (defensive only; should not happen via the
82
+ * public API).
73
83
  * @param changeId - Used as file description/slug (e.g., "auth-system", "add-user-auth")
74
84
  * @param segments - Optional path segments (first segment is typically the role: 'prd', 'rd', 'qa', etc.)
75
85
  * @returns Relative path to the artifact file
76
86
  */
77
- export function buildArtifactRelativePath(changeId, ...segments) {
87
+ export function buildArtifactRelativePathInRoot(projectRoot, changeId, ...segments) {
78
88
  validateChangeIdOrThrow(changeId);
79
- const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
80
- const sessionId = getSessionId(projectRoot);
89
+ const resolvedProjectRoot = projectRoot && projectRoot.length > 0
90
+ ? projectRoot
91
+ : (findProjectRoot(process.cwd()) ?? process.cwd());
92
+ const sessionId = getSessionId(resolvedProjectRoot);
81
93
  if (sessionId && segments.length > 0 && segments[0]) {
82
94
  const role = normalizeForwardSlashes(segments[0]);
83
- const dirPath = join(projectRoot, '.peaks', sessionId, role);
95
+ const dirPath = join(resolvedProjectRoot, '.peaks', sessionId, role);
84
96
  if (isUnsafeArtifactPath(role) || isUnsafeArtifactPath(sessionId)) {
85
97
  throw new ChangeIdValidationError(changeId);
86
98
  }
@@ -97,6 +109,28 @@ export function buildArtifactRelativePath(changeId, ...segments) {
97
109
  }
98
110
  return normalizeArtifactPath(candidatePath);
99
111
  }
112
+ /**
113
+ * Build an artifact-relative path using session-based storage.
114
+ *
115
+ * If a session exists, files are stored in:
116
+ * .peaks/<sessionId>/<role>/<number>-<changeId>.md
117
+ *
118
+ * If no session exists, falls back to legacy behavior:
119
+ * .peaks/<changeId>/<segments>
120
+ *
121
+ * This function walks `process.cwd()` to find the project root and reads
122
+ * `.peaks/.session.json` from it. Callers that already have an explicit
123
+ * `projectRoot` (workspace handle, test fixture, or CLI `--project` flag)
124
+ * should prefer `buildArtifactRelativePathInRoot(projectRoot, ...)` to
125
+ * avoid being polluted by the host environment's session binding.
126
+ *
127
+ * @param changeId - Used as file description/slug (e.g., "auth-system", "add-user-auth")
128
+ * @param segments - Optional path segments (first segment is typically the role: 'prd', 'rd', 'qa', etc.)
129
+ * @returns Relative path to the artifact file
130
+ */
131
+ export function buildArtifactRelativePath(changeId, ...segments) {
132
+ return buildArtifactRelativePathInRoot(findProjectRoot(process.cwd()) ?? process.cwd(), changeId, ...segments);
133
+ }
100
134
  export function isPathInsideArtifactRoot(path, artifactRoot) {
101
135
  if (!path || !artifactRoot)
102
136
  return false;
@@ -3,4 +3,4 @@ export declare const skillsDir: string;
3
3
  export declare const schemasDir: string;
4
4
  export declare const templatesDir: string;
5
5
  export declare const requiredSkillNames: readonly ["peaks-solo", "peaks-prd", "peaks-ui", "peaks-rd", "peaks-qa", "peaks-sc", "peaks-txt", "peaks-sop"];
6
- export declare const requiredSchemaFiles: readonly ["artifact-manifest.schema.json", "context-capsule.schema.json", "approval-record.schema.json", "change-impact.schema.json", "refactor-slice-spec.schema.json", "artifact-retention-report.schema.json", "capability-source.schema.json", "capability-item.schema.json", "capability-availability.schema.json", "recommendation-plan.schema.json", "artifact-workspace.schema.json", "mcp-server.schema.json", "mcp-install-spec.schema.json", "mcp-install-plan.schema.json", "mcp-apply-result.schema.json", "openspec-change-summary.schema.json", "openspec-render-request.schema.json", "openspec-validation-result.schema.json", "doctor-report.schema.json"];
6
+ export declare const requiredSchemaFiles: readonly ["artifact-manifest.schema.json", "context-capsule.schema.json", "approval-record.schema.json", "change-impact.schema.json", "refactor-slice-spec.schema.json", "artifact-retention-report.schema.json", "capability-source.schema.json", "capability-item.schema.json", "capability-availability.schema.json", "recommendation-plan.schema.json", "artifact-workspace.schema.json", "mcp-server.schema.json", "mcp-install-spec.schema.json", "mcp-install-plan.schema.json", "mcp-apply-result.schema.json", "openspec-change-summary.schema.json", "openspec-render-request.schema.json", "openspec-validation-result.schema.json", "doctor-report.schema.json", "library-breaking-changes.schema.json"];
@@ -45,5 +45,6 @@ export const requiredSchemaFiles = [
45
45
  'openspec-change-summary.schema.json',
46
46
  'openspec-render-request.schema.json',
47
47
  'openspec-validation-result.schema.json',
48
- 'doctor-report.schema.json'
48
+ 'doctor-report.schema.json',
49
+ 'library-breaking-changes.schema.json'
49
50
  ];
@@ -1 +1 @@
1
- export declare const CLI_VERSION = "1.2.8";
1
+ export declare const CLI_VERSION = "1.2.9";
@@ -1 +1 @@
1
- export const CLI_VERSION = "1.2.8";
1
+ export const CLI_VERSION = "1.2.9";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-cli",
3
- "version": "1.2.8",
3
+ "version": "1.2.9",
4
4
  "description": "Peaks CLI and short skill family for Claude Code automation.",
5
5
  "author": "SquabbyZ",
6
6
  "license": "MIT",
@@ -0,0 +1,141 @@
1
+ [
2
+ {
3
+ "library": "antd",
4
+ "fromMajor": 4,
5
+ "toMajor": 5,
6
+ "breakingChanges": [
7
+ { "api": "Drawer.width", "replacement": "Drawer.size", "since": "5.0.0" },
8
+ { "api": "Modal.width (string)", "replacement": "Modal.width (number) or styles.body.width (number)", "since": "5.0.0" },
9
+ { "api": "Form.create()", "replacement": "App.useForm() (under <App>)", "since": "5.0.0" },
10
+ { "api": "message.useMessage()", "replacement": "App.useApp().message (under <App>)", "since": "5.0.0" },
11
+ { "api": "notification.useNotification()", "replacement": "App.useApp().notification (under <App>)", "since": "5.0.0" },
12
+ { "api": "import 'antd/dist/antd.css'", "replacement": "import 'antd/dist/reset.css'", "since": "5.0.0" },
13
+ { "api": "<Form.Item {...layout}", "replacement": "<Form layout='vertical' | 'horizontal'> (no per-item layout prop)", "since": "5.0.0" }
14
+ ]
15
+ },
16
+ {
17
+ "library": "@antv/g6",
18
+ "fromMajor": 4,
19
+ "toMajor": 5,
20
+ "breakingChanges": [
21
+ { "api": "new G6.Graph({ mode: 'default' })", "replacement": "new G6.Graph({ type: 'graph' })", "since": "5.0.0" },
22
+ { "api": "graph.read(data)", "replacement": "graph.setData(data); graph.render()", "since": "5.0.0" },
23
+ { "api": "graph.changeData(data)", "replacement": "graph.setData(data); graph.render()", "since": "5.0.0" },
24
+ { "api": "G6.registerBehavior('drag-canvas', { getEvents() { return { ... } } })", "replacement": "G6.registerBehavior({ key: 'drag-canvas', type: 'drag-canvas', getEvents() { return { ... } } })", "since": "5.0.0" }
25
+ ]
26
+ },
27
+ {
28
+ "library": "@mui/material",
29
+ "fromMajor": 4,
30
+ "toMajor": 5,
31
+ "breakingChanges": [
32
+ { "api": "import { Grid } from '@mui/material'", "replacement": "import Grid from '@mui/material/Grid2' (Grid renamed Grid2, separate import path)", "since": "5.0.0" },
33
+ { "api": "createTheme({ unstable_createRheaTheme: true })", "replacement": "createTheme({ cssVariables: true })", "since": "5.0.0" },
34
+ { "api": "<DialogTitle disableTypography>", "replacement": "<DialogTitle component='div'> (disableTypography removed; use component prop)", "since": "5.0.0" },
35
+ { "api": "<TextField variant='standard' InputLabelProps={{ shrink: true }}>", "replacement": "<TextField variant='standard' slotProps={{ inputLabel: { shrink: true } }}>", "since": "5.0.0" }
36
+ ]
37
+ },
38
+ {
39
+ "library": "react-router-dom",
40
+ "fromMajor": 5,
41
+ "toMajor": 6,
42
+ "breakingChanges": [
43
+ { "api": "<Switch>", "replacement": "<Routes>", "since": "6.0.0" },
44
+ { "api": "<Redirect>", "replacement": "<Navigate>", "since": "6.0.0" },
45
+ { "api": "useHistory()", "replacement": "useNavigate()", "since": "6.0.0" },
46
+ { "api": "match.params", "replacement": "useParams()", "since": "6.0.0" },
47
+ { "api": "withRouter(Component)", "replacement": "useNavigate() + useLocation() + useParams() hooks", "since": "6.0.0" },
48
+ { "api": "<Route component={Component} />", "replacement": "<Route element={<Component />} /> (component prop renamed element)", "since": "6.0.0" },
49
+ { "api": "<Route path='/:id' exact />", "replacement": "<Route path='/:id' /> (exact prop removed; paths match exactly by default)", "since": "6.0.0" }
50
+ ]
51
+ },
52
+ {
53
+ "library": "tailwindcss",
54
+ "fromMajor": 2,
55
+ "toMajor": 3,
56
+ "breakingChanges": [
57
+ { "api": "tailwind.config.js (CommonJS, no 'type' in package.json)", "replacement": "tailwind.config.cjs (CommonJS) or tailwind.config.ts (ESM)", "since": "3.0.0" },
58
+ { "api": "content: ['./src/**/*.{html,js}']", "replacement": "content: ['./src/**/*.{html,js,jsx,ts,tsx,vue,svelte}']", "since": "3.0.0" },
59
+ { "api": "@tailwindcss/typography (default import)", "replacement": "import typography from '@tailwindcss/typography' (named default; config: { plugins: [typography] })", "since": "3.0.0" },
60
+ { "api": "colors: { 'custom-blue': '#1da1f2' }", "replacement": "colors: { custom: { blue: '#1da1f2' } } (nested under named palette; tailwind.config reads as 'text-custom-blue' = 'text-custom-blue' still works but is a single nesting level removed)", "since": "3.0.0" }
61
+ ]
62
+ },
63
+ {
64
+ "library": "@reduxjs/toolkit",
65
+ "fromMajor": 1,
66
+ "toMajor": 2,
67
+ "breakingChanges": [
68
+ { "api": "import { createAsyncThunk, createReducer } from '@reduxjs/toolkit' (with manual pending/fulfilled/rejected handlers)", "replacement": "import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; createSlice auto-generates pending/fulfilled/rejected matchers", "since": "2.0.0" },
69
+ { "api": "createSlice({ reducers: { ... }, extraReducers: (builder) => builder.addCase(...) })", "replacement": "createSlice({ reducers, extraReducers: (builder) => builder.addCase(...) }) — unchanged shape; check RTK 2.0 matchers for createAsyncThunk with builder.addMatcher", "since": "2.0.0" },
70
+ { "api": "configureStore({ reducer, middleware: (gDM) => gDM().concat(logger) })", "replacement": "configureStore({ reducer, middleware: (gDM) => gDM({ serializableCheck: false }).concat(logger) })", "since": "2.0.0" }
71
+ ]
72
+ },
73
+ {
74
+ "library": "next",
75
+ "fromMajor": 14,
76
+ "toMajor": 15,
77
+ "breakingChanges": [
78
+ { "api": "params (in pages/)", "replacement": "await params (params is now a Promise; await it before reading properties)", "since": "15.0.0" },
79
+ { "api": "searchParams (in pages/)", "replacement": "await searchParams (searchParams is now a Promise)", "since": "15.0.0" },
80
+ { "api": "cookies().get('name') (synchronous)", "replacement": "await cookies() (cookies() now returns a Promise; await before calling .get)", "since": "15.0.0" },
81
+ { "api": "headers().get('name') (synchronous)", "replacement": "await headers() (headers() now returns a Promise)", "since": "15.0.0" },
82
+ { "api": "fetch(url, { cache: 'force-cache' })", "replacement": "fetch(url, { cache: 'force-cache' }) + export const dynamic = 'force-static' OR use a Server Action", "since": "15.0.0" }
83
+ ]
84
+ },
85
+ {
86
+ "library": "react",
87
+ "fromMajor": 17,
88
+ "toMajor": 18,
89
+ "breakingChanges": [
90
+ { "api": "ReactDOM.render(<App />, container)", "replacement": "createRoot(container).render(<App />)", "since": "18.0.0" },
91
+ { "api": "ReactDOM.hydrate(<App />, container)", "replacement": "hydrateRoot(container, <App />)", "since": "18.0.0" },
92
+ { "api": "import { render } from 'react-dom'", "replacement": "import { createRoot } from 'react-dom/client' (separate entry point)", "since": "18.0.0" },
93
+ { "api": "ReactDOM.unmountComponentAtNode(container)", "replacement": "root.unmount() (returned from createRoot)", "since": "18.0.0" },
94
+ { "api": "defaultProps on function components", "replacement": "default parameters (defaultProps will be removed in a future major; use destructuring defaults: function Foo({ x = 1 } = {}) {})", "since": "18.3.0" }
95
+ ]
96
+ },
97
+ {
98
+ "library": "typescript",
99
+ "fromMajor": 4,
100
+ "toMajor": 5,
101
+ "breakingChanges": [
102
+ { "api": "import type { ... } from 'mod' (mixed type + value import without explicit type modifier)", "replacement": "Add `type` modifier to type-only imports explicitly (TS 5.0 enforces with verbatimModuleSyntax)", "since": "5.0.0" },
103
+ { "api": "namespace X { function f(): X.Type }", "replacement": "Use `import type` or `export type` from namespaces (TS 5.0 changes how namespaces interop with modules)", "since": "5.0.0" },
104
+ { "api": "experimentalDecorators metadata emit", "replacement": "Use the new TC39 stage-3 decorators; experimentalDecorators flag will be removed", "since": "5.0.0" },
105
+ { "api": "enum X { A, B = A * 2 } (numeric enum with computed member before non-computed)", "replacement": "Reorder enum members: place all non-computed members before computed ones", "since": "5.0.0" }
106
+ ]
107
+ },
108
+ {
109
+ "library": "prisma",
110
+ "fromMajor": 4,
111
+ "toMajor": 5,
112
+ "breakingChanges": [
113
+ { "api": "previewFeatures = ['xxx'] (in schema.prisma)", "replacement": "Move preview features to a separate block; some promoted to stable without flag", "since": "5.0.0" },
114
+ { "api": "prisma.user.findMany({ include: { posts: true } }) (implicit include via relation)", "replacement": "Use `with: { posts: true }` (include renamed to with)", "since": "5.0.0" },
115
+ { "api": "prisma.user.findMany({ relationJumps: { ... } })", "replacement": "Use `include` with `_count: { select: ... }` or nested select on relation fields", "since": "5.0.0" },
116
+ { "api": "@prisma/client v4 ESM/CJS interop (require vs import)", "replacement": "Pure ESM in v5; use `import` syntax only; dynamic require() of generated client throws", "since": "5.0.0" }
117
+ ]
118
+ },
119
+ {
120
+ "library": "eslint",
121
+ "fromMajor": 8,
122
+ "toMajor": 9,
123
+ "breakingChanges": [
124
+ { "api": ".eslintrc.json (legacy config)", "replacement": "eslint.config.js (flat config; legacy mode requires @eslint/eslintrc)", "since": "9.0.0" },
125
+ { "api": "module.exports = { rules: { ... } } (CommonJS rules)", "replacement": "export default [{ rules: { ... } }] (ESM flat config; rules must be inside an array entry)", "since": "9.0.0" },
126
+ { "api": "context.getSourceCode()", "replacement": "context.sourceCode (renamed; ESLint 9 deprecates getSourceCode)", "since": "9.0.0" },
127
+ { "api": "rule.create({ visitor }) without meta.schema or meta.type", "replacement": "rule.create({ meta: { schema: [...], type: 'problem'|'suggestion'|'layout' }, visitor }) (ESLint 9 requires meta)", "since": "9.0.0" }
128
+ ]
129
+ },
130
+ {
131
+ "library": "react-hook-form",
132
+ "fromMajor": 6,
133
+ "toMajor": 7,
134
+ "breakingChanges": [
135
+ { "api": "useForm({ defaultValues: { ... } }) (defaultValues is now required when using field arrays)", "replacement": "Always provide defaultValues; v7 removed the implicit empty default", "since": "7.0.0" },
136
+ { "api": "Controller({ name, control, render: ({ field }) => ... }) (Controller is now a generic — must pass field type)", "replacement": "Controller<FieldValues, TFieldPath>({ name: 'field' as TFieldPath, ... }) (cast path to TFieldPath)", "since": "7.0.0" },
137
+ { "api": "useFormContext() (no generic)", "replacement": "useFormContext<FieldValues>() (generic required for type inference)", "since": "7.0.0" },
138
+ { "api": "formState.errors.foo (loose type)", "replacement": "formState.errors.foo (typed as FieldError | undefined; use ?.message for safe access)", "since": "7.0.0" }
139
+ ]
140
+ }
141
+ ]
@@ -0,0 +1,6 @@
1
+ {
2
+ "lastUpdated": "2026-06-04",
3
+ "freshnessPolicyDays": 180,
4
+ "libraryCount": 12,
5
+ "rowCount": 55
6
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "Peaks Library Breaking Changes",
4
+ "description": "Manually-curated table of known breaking changes per (library, fromMajor, toMajor). Used by peaks-rd/qa to warn the LLM about deprecated APIs when writing code for projects on a newer major than the LLM's training data. Read by the LLM via the Read tool; the data file at library-breaking-changes.data.json holds the actual rows. Doctor validates parse-ability of this schema file.",
5
+ "type": "array",
6
+ "items": {
7
+ "type": "object",
8
+ "required": ["library", "fromMajor", "toMajor", "breakingChanges"],
9
+ "properties": {
10
+ "library": {
11
+ "type": "string",
12
+ "minLength": 1,
13
+ "description": "npm package name, e.g. 'antd', '@mui/material', 'react-router-dom'."
14
+ },
15
+ "fromMajor": {
16
+ "type": "integer",
17
+ "minimum": 0,
18
+ "description": "The major version the breaking change applies FROM. LLM cross-references (project major === toMajor) to fire the warning."
19
+ },
20
+ "toMajor": {
21
+ "type": "integer",
22
+ "minimum": 1,
23
+ "description": "The major version the breaking change applies TO (i.e. the new major where the API changed)."
24
+ },
25
+ "breakingChanges": {
26
+ "type": "array",
27
+ "items": {
28
+ "type": "object",
29
+ "required": ["api", "replacement", "since"],
30
+ "properties": {
31
+ "api": {
32
+ "type": "string",
33
+ "minLength": 1,
34
+ "description": "The deprecated/removed API call, e.g. 'Drawer.width'. Should be a substring that the LLM can grep for in the diff's import + JSX blocks."
35
+ },
36
+ "replacement": {
37
+ "type": "string",
38
+ "description": "The v2 API to use instead, e.g. 'Drawer.size'. If empty, the API is removed entirely (no v2 equivalent)."
39
+ },
40
+ "since": {
41
+ "type": "string",
42
+ "pattern": "^[0-9]+\\.[0-9]+(\\.[0-9]+)?$",
43
+ "description": "The semver string where this breaking change took effect, e.g. '5.0.0'. Matches the toMajor."
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
@@ -168,6 +168,17 @@ peaks openspec validate <change-id> --project <repo> --prefer-external --json
168
168
 
169
169
  # 4. generate test cases — MANDATORY, write to .peaks/<session-id>/qa/test-cases/<request-id>.md
170
170
  # categories: unit, integration, UI regression (frontend only)
171
+ #
172
+ # Optimization (slice 004): peaks-rd's parallel fan-out now includes a 4th
173
+ # sub-agent (`qa-test-cases-writer`) that pre-drafts this file at the
174
+ # end of RD implementation. If `.peaks/<sid>/qa/test-cases/<rid>.md`
175
+ # already exists when QA's main loop reaches this step, **QA does NOT
176
+ # re-draft it** — it just verifies the file is present and the
177
+ # per-criterion `ts` snippets are syntactically valid, then proceeds
178
+ # to step 5 (EXECUTE). The wall-clock win: QA's first action is
179
+ # "execute pre-drafted test plan" instead of "draft + execute".
180
+ # Fallback: if the file is missing (sub-agent failed / degraded to
181
+ # inline), QA drafts it inline as before.
171
182
 
172
183
  # 5. EXECUTE tests against the actual implementation — Peaks-Cli Gate A2
173
184
  # Run the project test command. Record output. Tests on paper are worthless.
@@ -458,6 +469,7 @@ QA cannot pass a change until the report contains evidence for every applicable
458
469
  5. **Browser-error feedback loop** — if Playwright MCP observation surfaces a page error, console exception, broken network request, hydration/render failure, or visible regression, return the work to RD/development with the exact evidence. Do not pass QA until the fixed build is retested in the browser.
459
470
  6. **Security check** — run security review for the changed surface and dependency/config changes. Record findings, fixes, and unresolved risks.
460
471
  7. **Performance check** — run the project’s available performance check, build-size check, Lighthouse-equivalent check, or browser performance inspection appropriate to the change. Record baseline/after numbers when available.
472
+ 8. **Library version regressions** — when the slice's diff contains an `import` statement that matches a `breakingChanges[].api` entry in `schemas/library-breaking-changes.data.json` for the library's installed major (read from the RD-handoff's `## Library versions` section), record a `## Library version regressions` block in `qa/test-reports/<rid>.md` listing each hit. Per row: `<api>` → `<replacement>`, source `schemas/library-breaking-changes.data.json`. Treat each unreplaced hit as a **return-to-rd** reason — the LLM should fix the diff before re-handoff. (This is the QA-side counterpart of the RD `## Library version awareness` preflight; the two together form a check-and-verify pair.)
461
473
  8. **Validation report** — write or link a report containing scope, environment, commands, sanitized browser evidence, security/performance results, pass/fail summary, residual risks, and next action.
462
474
  9. **Acceptance coverage** — every PRD acceptance item has at least one linked QA test case (`peaks scan acceptance-coverage --rid <rid>`). **→ verified by Peaks-Cli Gate E**. This is the deterministic check that no requirement was forgotten between PRD and verdict.
463
475
  10. **QA artifact lint** — the QA request artifact body has no unfilled placeholders (`peaks request lint <rid> --role qa`). **→ verified by Peaks-Cli Gate F**. Catches the "wrote the template, forgot to fill it" failure mode that template-style reports invite.