peaks-cli 1.4.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/.claude-plugin/marketplace.json +51 -0
  2. package/CHANGELOG.md +238 -0
  3. package/README-en.md +226 -0
  4. package/README.md +152 -122
  5. package/dist/src/cli/commands/agent-commands.d.ts +20 -0
  6. package/dist/src/cli/commands/agent-commands.js +48 -0
  7. package/dist/src/cli/commands/audit-commands.d.ts +18 -0
  8. package/dist/src/cli/commands/audit-commands.js +138 -0
  9. package/dist/src/cli/commands/classify-classify-commands.d.ts +19 -0
  10. package/dist/src/cli/commands/classify-classify-commands.js +151 -0
  11. package/dist/src/cli/commands/code-review-commands.d.ts +34 -0
  12. package/dist/src/cli/commands/code-review-commands.js +83 -0
  13. package/dist/src/cli/commands/config-commands.js +90 -0
  14. package/dist/src/cli/commands/context-commands.d.ts +21 -0
  15. package/dist/src/cli/commands/context-commands.js +167 -0
  16. package/dist/src/cli/commands/core-artifact-commands.js +60 -2
  17. package/dist/src/cli/commands/hook-handle.js +50 -0
  18. package/dist/src/cli/commands/loop-commands.d.ts +21 -0
  19. package/dist/src/cli/commands/loop-commands.js +128 -0
  20. package/dist/src/cli/commands/openspec-commands.js +37 -0
  21. package/dist/src/cli/commands/preferences-commands.d.ts +2 -0
  22. package/dist/src/cli/commands/preferences-commands.js +147 -0
  23. package/dist/src/cli/commands/skill-conformance-commands.d.ts +9 -0
  24. package/dist/src/cli/commands/skill-conformance-commands.js +39 -0
  25. package/dist/src/cli/commands/understand-commands.js +34 -0
  26. package/dist/src/cli/commands/upgrade-commands.d.ts +23 -0
  27. package/dist/src/cli/commands/upgrade-commands.js +57 -0
  28. package/dist/src/cli/commands/workflow-commands.js +70 -0
  29. package/dist/src/cli/commands/workspace-commands.js +86 -0
  30. package/dist/src/cli/program.js +30 -0
  31. package/dist/src/services/agent/ecc-agent-service.d.ts +47 -0
  32. package/dist/src/services/agent/ecc-agent-service.js +143 -0
  33. package/dist/src/services/artifacts/request-artifact-service.js +14 -0
  34. package/dist/src/services/audit/backing-detector.d.ts +24 -0
  35. package/dist/src/services/audit/backing-detector.js +59 -0
  36. package/dist/src/services/audit/classifier.d.ts +38 -0
  37. package/dist/src/services/audit/classifier.js +127 -0
  38. package/dist/src/services/audit/enforcers/active-skill-resolver.d.ts +29 -0
  39. package/dist/src/services/audit/enforcers/active-skill-resolver.js +71 -0
  40. package/dist/src/services/audit/enforcers/design-draft-confirm.d.ts +25 -0
  41. package/dist/src/services/audit/enforcers/design-draft-confirm.js +54 -0
  42. package/dist/src/services/audit/enforcers/lint-audit-regression.d.ts +21 -0
  43. package/dist/src/services/audit/enforcers/lint-audit-regression.js +86 -0
  44. package/dist/src/services/audit/enforcers/lint-catalog-governance.d.ts +27 -0
  45. package/dist/src/services/audit/enforcers/lint-catalog-governance.js +38 -0
  46. package/dist/src/services/audit/enforcers/lint-cli-back.d.ts +16 -0
  47. package/dist/src/services/audit/enforcers/lint-cli-back.js +35 -0
  48. package/dist/src/services/audit/enforcers/lint-output-style.d.ts +11 -0
  49. package/dist/src/services/audit/enforcers/lint-output-style.js +94 -0
  50. package/dist/src/services/audit/enforcers/lint-reference-integrity.d.ts +6 -0
  51. package/dist/src/services/audit/enforcers/lint-reference-integrity.js +83 -0
  52. package/dist/src/services/audit/enforcers/lint-reference-shape.d.ts +30 -0
  53. package/dist/src/services/audit/enforcers/lint-reference-shape.js +272 -0
  54. package/dist/src/services/audit/enforcers/lint-style.d.ts +49 -0
  55. package/dist/src/services/audit/enforcers/lint-style.js +173 -0
  56. package/dist/src/services/audit/enforcers/lint-workflow-shape.d.ts +5 -0
  57. package/dist/src/services/audit/enforcers/lint-workflow-shape.js +141 -0
  58. package/dist/src/services/audit/enforcers/login-gate.d.ts +23 -0
  59. package/dist/src/services/audit/enforcers/login-gate.js +40 -0
  60. package/dist/src/services/audit/enforcers/mock-placement.d.ts +25 -0
  61. package/dist/src/services/audit/enforcers/mock-placement.js +48 -0
  62. package/dist/src/services/audit/enforcers/no-root-pollution.d.ts +21 -0
  63. package/dist/src/services/audit/enforcers/no-root-pollution.js +56 -0
  64. package/dist/src/services/audit/enforcers/pre-rd-scan.d.ts +22 -0
  65. package/dist/src/services/audit/enforcers/pre-rd-scan.js +23 -0
  66. package/dist/src/services/audit/enforcers/prototype-fidelity.d.ts +25 -0
  67. package/dist/src/services/audit/enforcers/prototype-fidelity.js +75 -0
  68. package/dist/src/services/audit/enforcers/resume-detection.d.ts +21 -0
  69. package/dist/src/services/audit/enforcers/resume-detection.js +52 -0
  70. package/dist/src/services/audit/enforcers/solo-code-ban.d.ts +23 -0
  71. package/dist/src/services/audit/enforcers/solo-code-ban.js +27 -0
  72. package/dist/src/services/audit/enforcers/sub-agent-sid.d.ts +25 -0
  73. package/dist/src/services/audit/enforcers/sub-agent-sid.js +63 -0
  74. package/dist/src/services/audit/enforcers/tech-doc-presence.d.ts +28 -0
  75. package/dist/src/services/audit/enforcers/tech-doc-presence.js +35 -0
  76. package/dist/src/services/audit/red-line-catalog-p2-a.d.ts +21 -0
  77. package/dist/src/services/audit/red-line-catalog-p2-a.js +233 -0
  78. package/dist/src/services/audit/red-line-catalog-p2-b.d.ts +19 -0
  79. package/dist/src/services/audit/red-line-catalog-p2-b.js +225 -0
  80. package/dist/src/services/audit/red-line-catalog.d.ts +51 -0
  81. package/dist/src/services/audit/red-line-catalog.js +210 -0
  82. package/dist/src/services/audit/red-lines-service.d.ts +23 -0
  83. package/dist/src/services/audit/red-lines-service.js +486 -0
  84. package/dist/src/services/audit/scanners/openspec-scanner.d.ts +15 -0
  85. package/dist/src/services/audit/scanners/openspec-scanner.js +55 -0
  86. package/dist/src/services/audit/scanners/rules-tree-scanner.d.ts +16 -0
  87. package/dist/src/services/audit/scanners/rules-tree-scanner.js +56 -0
  88. package/dist/src/services/audit/scanners/skills-tree-scanner.d.ts +17 -0
  89. package/dist/src/services/audit/scanners/skills-tree-scanner.js +46 -0
  90. package/dist/src/services/audit/static-service.d.ts +57 -0
  91. package/dist/src/services/audit/static-service.js +125 -0
  92. package/dist/src/services/audit/types.d.ts +69 -0
  93. package/dist/src/services/audit/types.js +13 -0
  94. package/dist/src/services/classify/classify-service.d.ts +42 -0
  95. package/dist/src/services/classify/classify-service.js +122 -0
  96. package/dist/src/services/classify/classify-types.d.ts +79 -0
  97. package/dist/src/services/classify/classify-types.js +90 -0
  98. package/dist/src/services/code-review/ocr-service.d.ts +129 -0
  99. package/dist/src/services/code-review/ocr-service.js +362 -0
  100. package/dist/src/services/config/config-migration.d.ts +32 -0
  101. package/dist/src/services/config/config-migration.js +92 -0
  102. package/dist/src/services/config/config-restore.d.ts +10 -0
  103. package/dist/src/services/config/config-restore.js +47 -0
  104. package/dist/src/services/config/config-rollback.d.ts +13 -0
  105. package/dist/src/services/config/config-rollback.js +26 -0
  106. package/dist/src/services/config/config-service.d.ts +35 -2
  107. package/dist/src/services/config/config-service.js +81 -0
  108. package/dist/src/services/config/config-types.d.ts +58 -0
  109. package/dist/src/services/config/config-types.js +6 -0
  110. package/dist/src/services/doctor/doctor-service.js +96 -0
  111. package/dist/src/services/ide/adapters/hermes-adapter.d.ts +21 -0
  112. package/dist/src/services/ide/adapters/hermes-adapter.js +51 -0
  113. package/dist/src/services/ide/adapters/openclaw-adapter.d.ts +14 -0
  114. package/dist/src/services/ide/adapters/openclaw-adapter.js +42 -0
  115. package/dist/src/services/ide/ide-registry.js +7 -0
  116. package/dist/src/services/ide/ide-types.d.ts +1 -1
  117. package/dist/src/services/openspec/openspec-propose-from-doctor-service.d.ts +31 -0
  118. package/dist/src/services/openspec/openspec-propose-from-doctor-service.js +95 -0
  119. package/dist/src/services/preferences/preferences-service.d.ts +6 -0
  120. package/dist/src/services/preferences/preferences-service.js +43 -0
  121. package/dist/src/services/preferences/preferences-types.d.ts +90 -0
  122. package/dist/src/services/preferences/preferences-types.js +38 -0
  123. package/dist/src/services/skills/skill-conformance-service.d.ts +40 -0
  124. package/dist/src/services/skills/skill-conformance-service.js +136 -0
  125. package/dist/src/services/skills/skill-runbook-service.js +44 -10
  126. package/dist/src/services/skills/sync-service.d.ts +43 -0
  127. package/dist/src/services/skills/sync-service.js +99 -0
  128. package/dist/src/services/slice/slice-check-service.js +166 -13
  129. package/dist/src/services/slice/slice-check-types.d.ts +1 -1
  130. package/dist/src/services/standards/migrate-claude-rules-service.d.ts +19 -0
  131. package/dist/src/services/standards/migrate-claude-rules-service.js +193 -0
  132. package/dist/src/services/understand/understand-scan-service.js +15 -2
  133. package/dist/src/services/understand/understand-types.d.ts +26 -0
  134. package/dist/src/services/upgrade/1x-detector-service.d.ts +7 -0
  135. package/dist/src/services/upgrade/1x-detector-service.js +94 -0
  136. package/dist/src/services/upgrade/gitignore-migrate-service.d.ts +56 -0
  137. package/dist/src/services/upgrade/gitignore-migrate-service.js +170 -0
  138. package/dist/src/services/upgrade/upgrade-service.d.ts +47 -0
  139. package/dist/src/services/upgrade/upgrade-service.js +381 -0
  140. package/dist/src/services/workspace/sid-naming-guard.d.ts +14 -0
  141. package/dist/src/services/workspace/sid-naming-guard.js +31 -0
  142. package/dist/src/services/workspace/workspace-archive-service.d.ts +19 -0
  143. package/dist/src/services/workspace/workspace-archive-service.js +32 -0
  144. package/dist/src/services/workspace/workspace-clean-service.d.ts +41 -0
  145. package/dist/src/services/workspace/workspace-clean-service.js +86 -0
  146. package/dist/src/services/workspace/workspace-state-service.d.ts +7 -0
  147. package/dist/src/services/workspace/workspace-state-service.js +43 -0
  148. package/dist/src/shared/change-id.js +4 -1
  149. package/dist/src/shared/version.d.ts +1 -1
  150. package/dist/src/shared/version.js +1 -1
  151. package/package.json +8 -2
  152. package/schemas/doctor-report.schema.json +1 -1
  153. package/scripts/install-skills.mjs +296 -12
  154. package/skills/peaks-doctor/SKILL.md +59 -0
  155. package/skills/peaks-doctor/references/doctor-check-catalog.md +31 -0
  156. package/skills/peaks-doctor/references/from-doctor-flow.md +64 -0
  157. package/skills/peaks-doctor/test_prompts.json +17 -0
  158. package/skills/peaks-ide/SKILL.md +2 -0
  159. package/skills/peaks-qa/SKILL.md +9 -7
  160. package/skills/peaks-qa/references/artifact-per-request.md +19 -5
  161. package/skills/peaks-qa/references/qa-perf-test-plan.md +6 -6
  162. package/skills/peaks-qa/references/qa-runbook.md +1 -1
  163. package/skills/peaks-rd/SKILL.md +25 -10
  164. package/skills/peaks-rd/references/ocr-integration.md +214 -0
  165. package/skills/peaks-rd/references/rd-fanout-contracts.md +70 -0
  166. package/skills/peaks-rd/references/rd-runbook.md +1 -1
  167. package/skills/peaks-solo/SKILL.md +10 -4
  168. package/skills/peaks-solo/references/step-0-55-1x-detection.md +82 -0
  169. package/skills/peaks-solo/references/workflow-gates-and-types.md +9 -0
@@ -0,0 +1,31 @@
1
+ /**
2
+ * SID naming guard. Enforces the "two-axis" convention from spec §0:
3
+ * session id: YYYY-MM-DD-session-<3-6 chars lowercase alnum>
4
+ * change id: kebab-case
5
+ *
6
+ * Spec §8.7 — bare forms (sid-3 / sid-h / sid-r / unknown-sid) are
7
+ * migrated to `_archive/invalid-sids/`, NOT tolerated.
8
+ */
9
+ export const SID_FORMAT_DESCRIPTION = '<YYYY-MM-DD>-session-<3-6 chars lowercase alnum>, e.g. 2026-06-11-session-abc123';
10
+ const VALID_SID_REGEX = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])-session-[0-9a-z]{3,6}$/;
11
+ const BARE_SID_REGEX = /^(sid-[a-z0-9]+|unknown-sid)$/;
12
+ const VALID_CHANGE_ID_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
13
+ export function isValidSessionId(sid) {
14
+ return VALID_SID_REGEX.test(sid);
15
+ }
16
+ export function isValidChangeId(cid) {
17
+ return VALID_CHANGE_ID_REGEX.test(cid);
18
+ }
19
+ export function isBareSid(name) {
20
+ return BARE_SID_REGEX.test(name);
21
+ }
22
+ export function assertValidSessionId(sid) {
23
+ if (!isValidSessionId(sid)) {
24
+ throw new Error(`NAMING_INVALID: session id "${sid}" does not match required format ${SID_FORMAT_DESCRIPTION}`);
25
+ }
26
+ }
27
+ export function assertValidChangeId(cid) {
28
+ if (!isValidChangeId(cid)) {
29
+ throw new Error(`NAMING_INVALID: change id "${cid}" must be kebab-case (lowercase alnum and dashes only)`);
30
+ }
31
+ }
@@ -0,0 +1,19 @@
1
+ export interface ArchivePlan {
2
+ sid: string;
3
+ sourcePath: string;
4
+ targetPath: string;
5
+ sourceExists: boolean;
6
+ }
7
+ export interface ArchiveOptions {
8
+ sid: string;
9
+ apply: boolean;
10
+ }
11
+ export interface ArchiveResult {
12
+ moved: string[];
13
+ skipped: {
14
+ sid: string;
15
+ reason: string;
16
+ }[];
17
+ }
18
+ export declare function planArchive(projectRoot: string, sid: string): ArchivePlan;
19
+ export declare function archiveSession(projectRoot: string, options: ArchiveOptions): ArchiveResult;
@@ -0,0 +1,32 @@
1
+ import { existsSync, mkdirSync, renameSync } from 'node:fs';
2
+ import { join, sep } from 'node:path';
3
+ import { assertValidSessionId } from './sid-naming-guard.js';
4
+ const ARCHIVE_ROOT = '_archive';
5
+ const RUNTIME_DIR = '_runtime';
6
+ function toPosix(p) {
7
+ return p.split(sep).join('/');
8
+ }
9
+ export function planArchive(projectRoot, sid) {
10
+ assertValidSessionId(sid);
11
+ const yyyyMm = sid.slice(0, 7);
12
+ const sourcePath = join(projectRoot, '.peaks', RUNTIME_DIR, sid);
13
+ const targetPath = join(projectRoot, '.peaks', ARCHIVE_ROOT, yyyyMm, sid);
14
+ return {
15
+ sid,
16
+ sourcePath: toPosix(sourcePath),
17
+ targetPath: toPosix(targetPath),
18
+ sourceExists: existsSync(sourcePath),
19
+ };
20
+ }
21
+ export function archiveSession(projectRoot, options) {
22
+ const plan = planArchive(projectRoot, options.sid);
23
+ if (!plan.sourceExists) {
24
+ return { moved: [], skipped: [{ sid: options.sid, reason: 'source does not exist' }] };
25
+ }
26
+ if (!options.apply) {
27
+ return { moved: [], skipped: [{ sid: options.sid, reason: 'dry-run' }] };
28
+ }
29
+ mkdirSync(join(plan.targetPath, '..'), { recursive: true });
30
+ renameSync(plan.sourcePath, plan.targetPath);
31
+ return { moved: [options.sid], skipped: [] };
32
+ }
@@ -0,0 +1,41 @@
1
+ export interface RuntimeSessionInfo {
2
+ sid: string;
3
+ mtimeMs: number;
4
+ ageHours: number;
5
+ }
6
+ export interface CleanupOptions {
7
+ olderThanHours: number;
8
+ graceHours: number;
9
+ }
10
+ export interface CleanupResult {
11
+ deleted: string[];
12
+ skipped: {
13
+ sid: string;
14
+ reason: string;
15
+ }[];
16
+ }
17
+ export declare function runtimeDirPath(projectRoot: string): string;
18
+ export declare function listRuntimeSessions(projectRoot: string): RuntimeSessionInfo[];
19
+ export declare function planRuntimeCleanup(sessions: RuntimeSessionInfo[], options: CleanupOptions): {
20
+ eligible: string[];
21
+ skipped: {
22
+ sid: string;
23
+ reason: string;
24
+ }[];
25
+ };
26
+ export declare function executeRuntimeCleanup(projectRoot: string, options: CleanupOptions & {
27
+ apply: boolean;
28
+ }): CleanupResult;
29
+ export interface SubAgentInvalidPlan {
30
+ invalid: string[];
31
+ invalidSidFormat: string[];
32
+ }
33
+ export declare function subAgentDirPath(projectRoot: string): string;
34
+ export declare function invalidSidsArchivePath(projectRoot: string): string;
35
+ export declare function listInvalidSubAgentSids(projectRoot: string): string[];
36
+ export declare function executeSubAgentClean(projectRoot: string, options: {
37
+ apply: boolean;
38
+ }): {
39
+ moved: string[];
40
+ skipped: string[];
41
+ };
@@ -0,0 +1,86 @@
1
+ import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { isBareSid, isValidSessionId } from './sid-naming-guard.js';
4
+ const RUNTIME_DIR = '_runtime';
5
+ export function runtimeDirPath(projectRoot) {
6
+ return join(projectRoot, '.peaks', RUNTIME_DIR);
7
+ }
8
+ export function listRuntimeSessions(projectRoot) {
9
+ const dir = runtimeDirPath(projectRoot);
10
+ if (!existsSync(dir))
11
+ return [];
12
+ const now = Date.now();
13
+ return readdirSync(dir, { withFileTypes: true })
14
+ .filter((e) => e.isDirectory())
15
+ .map((e) => {
16
+ const sid = e.name;
17
+ const fullPath = join(dir, sid);
18
+ const stat = statSync(fullPath);
19
+ const ageHours = (now - stat.mtimeMs) / (1000 * 3600);
20
+ return { sid, mtimeMs: stat.mtimeMs, ageHours };
21
+ });
22
+ }
23
+ export function planRuntimeCleanup(sessions, options) {
24
+ const eligible = [];
25
+ const skipped = [];
26
+ const cutoffHours = options.olderThanHours + options.graceHours;
27
+ for (const s of sessions) {
28
+ if (s.ageHours >= cutoffHours) {
29
+ eligible.push(s.sid);
30
+ }
31
+ else {
32
+ skipped.push({ sid: s.sid, reason: `fresh: age=${s.ageHours.toFixed(1)}h < cutoff=${cutoffHours}h` });
33
+ }
34
+ }
35
+ return { eligible, skipped };
36
+ }
37
+ export function executeRuntimeCleanup(projectRoot, options) {
38
+ const sessions = listRuntimeSessions(projectRoot);
39
+ const plan = planRuntimeCleanup(sessions, options);
40
+ if (options.apply) {
41
+ const dir = runtimeDirPath(projectRoot);
42
+ for (const sid of plan.eligible) {
43
+ rmSync(join(dir, sid), { recursive: true, force: true });
44
+ }
45
+ }
46
+ return { deleted: plan.eligible, skipped: plan.skipped };
47
+ }
48
+ const SUBAGENT_DIR = '_sub_agents';
49
+ const INVALID_ARCHIVE = '_archive/invalid-sids';
50
+ export function subAgentDirPath(projectRoot) {
51
+ return join(projectRoot, '.peaks', SUBAGENT_DIR);
52
+ }
53
+ export function invalidSidsArchivePath(projectRoot) {
54
+ return join(projectRoot, '.peaks', INVALID_ARCHIVE);
55
+ }
56
+ export function listInvalidSubAgentSids(projectRoot) {
57
+ const dir = subAgentDirPath(projectRoot);
58
+ if (!existsSync(dir))
59
+ return [];
60
+ return readdirSync(dir, { withFileTypes: true })
61
+ .filter((e) => e.isDirectory())
62
+ .map((e) => e.name)
63
+ .filter((name) => isBareSid(name) || !isValidSessionId(name));
64
+ }
65
+ export function executeSubAgentClean(projectRoot, options) {
66
+ const invalid = listInvalidSubAgentSids(projectRoot);
67
+ const moved = [];
68
+ if (options.apply && invalid.length > 0) {
69
+ const archiveDir = invalidSidsArchivePath(projectRoot);
70
+ mkdirSync(archiveDir, { recursive: true });
71
+ for (const sid of invalid) {
72
+ const from = join(subAgentDirPath(projectRoot), sid);
73
+ const to = join(archiveDir, sid);
74
+ if (existsSync(to)) {
75
+ // collision — append timestamp suffix
76
+ const stamped = `${sid}-${Date.now()}`;
77
+ renameSync(from, join(archiveDir, stamped));
78
+ }
79
+ else {
80
+ renameSync(from, to);
81
+ }
82
+ moved.push(sid);
83
+ }
84
+ }
85
+ return { moved: options.apply ? moved : invalid, skipped: [] };
86
+ }
@@ -0,0 +1,7 @@
1
+ export declare function isLegacyDecisionDotfile(name: string): boolean;
2
+ export declare function stateDirPath(projectRoot: string): string;
3
+ export interface CollectResult {
4
+ moved: string[];
5
+ skipped: string[];
6
+ }
7
+ export declare function collectLegacyDecisionDotfiles(projectRoot: string): CollectResult;
@@ -0,0 +1,43 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync } from 'node:fs';
2
+ import { join, posix } from 'node:path';
3
+ /**
4
+ * Spec §8.4 + §8.5 — `.peaks/_state/` collects one-time decision dotfiles.
5
+ * Migrates from legacy `.peaks/<name>` flat layout.
6
+ *
7
+ * `stateDirPath` returns a POSIX-normalized logical path so callers and tests
8
+ * can compare against a platform-independent string (the .peaks tree is a
9
+ * config key, not a direct fs call). `mkdirSync` / `renameSync` below use
10
+ * the platform-native `join` so files actually land on disk.
11
+ */
12
+ const LEGACY_DOTFILES = [
13
+ '.peaks-init-hooks-decision.json',
14
+ '.peaks-openspec-opt-in.json',
15
+ ];
16
+ const STATE_DIR_NAME = '_state';
17
+ export function isLegacyDecisionDotfile(name) {
18
+ return LEGACY_DOTFILES.includes(name);
19
+ }
20
+ export function stateDirPath(projectRoot) {
21
+ return posix.join(projectRoot, '.peaks', STATE_DIR_NAME);
22
+ }
23
+ export function collectLegacyDecisionDotfiles(projectRoot) {
24
+ const peaksDir = join(projectRoot, '.peaks');
25
+ // Use the platform-native join for actual filesystem operations;
26
+ // `stateDirPath` (POSIX) is only the public/portable surface for callers.
27
+ const stateDir = join(projectRoot, '.peaks', STATE_DIR_NAME);
28
+ mkdirSync(stateDir, { recursive: true });
29
+ const moved = [];
30
+ const skipped = [];
31
+ for (const name of LEGACY_DOTFILES) {
32
+ const from = join(peaksDir, name);
33
+ const to = join(stateDir, name);
34
+ if (!existsSync(from))
35
+ continue;
36
+ if (existsSync(to)) {
37
+ throw new Error(`DOTFILE_COLLISION: ${name} already exists in ${stateDir} (${readFileSync(to, 'utf8').length} bytes); refusing to overwrite`);
38
+ }
39
+ renameSync(from, to);
40
+ moved.push(name);
41
+ }
42
+ return { moved, skipped };
43
+ }
@@ -303,7 +303,10 @@ export function setCurrentChangeId(projectRoot, changeId, options = {}) {
303
303
  catch { /* best effort */ }
304
304
  }
305
305
  }
306
- symlinkSync(targetDir, bindingPath);
306
+ // On Windows, use a 'junction' (directory hard link) which doesn't
307
+ // require developer mode / admin. POSIX uses a regular 'dir' symlink.
308
+ const linkType = process.platform === 'win32' ? 'junction' : 'dir';
309
+ symlinkSync(targetDir, bindingPath, linkType);
307
310
  }
308
311
  /**
309
312
  * Canonical on-disk path to a change-id's reviewable artifacts
@@ -1 +1 @@
1
- export declare const CLI_VERSION = "1.4.2";
1
+ export declare const CLI_VERSION = "2.0.0";
@@ -1 +1 @@
1
- export const CLI_VERSION = "1.4.2";
1
+ export const CLI_VERSION = "2.0.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-cli",
3
- "version": "1.4.2",
3
+ "version": "2.0.0",
4
4
  "description": "Cross-AI-IDE workflow-gating CLI + skill family (Claude Code shipped, Trae in progress; Codex / Cursor / Qoder / Tongyi Lingma on the roadmap).",
5
5
  "author": "SquabbyZ",
6
6
  "license": "MIT",
@@ -33,7 +33,12 @@
33
33
  "!skills/**/test-prompts.json",
34
34
  "!skills/**/.DS_Store",
35
35
  "output-styles/**",
36
- "schemas/*.json"
36
+ "schemas/*.json",
37
+ ".claude-plugin/**",
38
+ "README.md",
39
+ "README-en.md",
40
+ "CHANGELOG.md",
41
+ "LICENSE"
37
42
  ],
38
43
  "scripts": {
39
44
  "build": "node ./scripts/sync-version.mjs && node ./scripts/clean-dist.mjs && tsc -p tsconfig.json",
@@ -54,6 +59,7 @@
54
59
  "node": ">=20.0.0"
55
60
  },
56
61
  "dependencies": {
62
+ "@alibaba-group/open-code-review": "1.3.1",
57
63
  "@colbymchenry/codegraph": "0.7.10",
58
64
  "commander": "^12.1.0",
59
65
  "fzf": "^0.5.2",
@@ -13,7 +13,7 @@
13
13
  "properties": {
14
14
  "id": {
15
15
  "type": "string",
16
- "pattern": "^(skill|skill-name|skill-parse|skill-runbook|skill-apply-note|skill-presence|statusline|schema|config|doctor-self|capability|build|integration):[A-Za-z0-9][A-Za-z0-9._-]*$",
16
+ "pattern": "^(skill|skill-name|skill-parse|skill-runbook|skill-apply-note|skill-presence|statusline|schema|config|doctor-self|capability|build|integration|L3):[A-Za-z0-9][A-Za-z0-9._-]*$",
17
17
  "description": "Stable check id. Known prefixes: skill:<name> (required skill present), skill-name:<dir> (directory matches declared name), skill-parse:<dir> (skill metadata parsed), skill-runbook:<name> (Default runbook section exists), skill-apply-note:<name> (destructive --apply lines carry an authorization/--dry-run note), skill-presence:<topic> (status of .peaks/.active-skill.json — current/freshness/workspace), statusline:<topic> (out-of-band Claude Code statusLine — install/runtime), schema:<file> (schema file exists and is valid JSON), config:<scope> (optional config locations), doctor-self:<topic> (doctor validates its own output against this schema), capability:<name> (third-party capability is resolvable at the pinned version), build:<topic> (build-hygiene checks — dist/source version consistency), integration:<name> (third-party integration probe — e.g. gateguard-fact-force PreToolUse hook conflict on .peaks/**)."
18
18
  },
19
19
  "ok": { "type": "boolean" },
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { closeSync, constants, existsSync, fchmodSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readlinkSync, realpathSync, readdirSync, renameSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { spawnSync } from 'node:child_process';
3
4
  import { createHash, randomUUID } from 'node:crypto';
4
5
  import { homedir } from 'node:os';
5
6
  import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
@@ -389,6 +390,19 @@ const IDE_DETECTION_DIRS = [
389
390
  { id: 'tongyi-lingma', dir: '.tongyi-lingma' },
390
391
  ];
391
392
 
393
+ /**
394
+ * Per-IDE skill install paths. Per peaks-cli tenet
395
+ * "minimal-user-operation" (2026-06-11), the user should
396
+ * never have to run a per-platform install command — the
397
+ * `npm i -g peaks-cli` postinstall iterates ALL of these
398
+ * and symlinks the peaks-* skill family to every platform
399
+ * the user might be on.
400
+ *
401
+ * 1.x had only `claude-code` (the other 5 entries were
402
+ * `null`); real Trae users reported the Trae skill
403
+ * directory was never populated. 2.0 fixes this by giving
404
+ * all 8 platforms canonical install paths.
405
+ */
392
406
  const IDE_SKILL_INSTALL_PROFILES = {
393
407
  'claude-code': {
394
408
  skillsDir: join(homedir(), '.claude', 'skills'),
@@ -396,11 +410,48 @@ const IDE_SKILL_INSTALL_PROFILES = {
396
410
  envVar: 'PEAKS_CLAUDE_SKILLS_DIR',
397
411
  outputStylesEnvVar: 'PEAKS_CLAUDE_OUTPUT_STYLES_DIR',
398
412
  },
399
- 'trae': null,
400
- 'codex': null,
401
- 'cursor': null,
402
- 'qoder': null,
403
- 'tongyi-lingma': null,
413
+ 'trae': {
414
+ skillsDir: join(homedir(), '.trae', 'skills'),
415
+ outputStylesDir: join(homedir(), '.trae', 'output-styles'),
416
+ envVar: 'PEAKS_TRAE_SKILLS_DIR',
417
+ outputStylesEnvVar: 'PEAKS_TRAE_OUTPUT_STYLES_DIR',
418
+ },
419
+ 'codex': {
420
+ skillsDir: join(homedir(), '.codex', 'skills'),
421
+ outputStylesDir: join(homedir(), '.codex', 'output-styles'),
422
+ envVar: 'PEAKS_CODEX_SKILLS_DIR',
423
+ outputStylesEnvVar: 'PEAKS_CODEX_OUTPUT_STYLES_DIR',
424
+ },
425
+ 'cursor': {
426
+ skillsDir: join(homedir(), '.cursor', 'skills'),
427
+ outputStylesDir: join(homedir(), '.cursor', 'output-styles'),
428
+ envVar: 'PEAKS_CURSOR_SKILLS_DIR',
429
+ outputStylesEnvVar: 'PEAKS_CURSOR_OUTPUT_STYLES_DIR',
430
+ },
431
+ 'qoder': {
432
+ skillsDir: join(homedir(), '.qoder', 'skills'),
433
+ outputStylesDir: join(homedir(), '.qoder', 'output-styles'),
434
+ envVar: 'PEAKS_QODER_SKILLS_DIR',
435
+ outputStylesEnvVar: 'PEAKS_QODER_OUTPUT_STYLES_DIR',
436
+ },
437
+ 'tongyi-lingma': {
438
+ skillsDir: join(homedir(), '.tongyi-lingma', 'skills'),
439
+ outputStylesDir: join(homedir(), '.tongyi-lingma', 'output-styles'),
440
+ envVar: 'PEAKS_TONGYI_SKILLS_DIR',
441
+ outputStylesEnvVar: 'PEAKS_TONGYI_OUTPUT_STYLES_DIR',
442
+ },
443
+ 'hermes': {
444
+ skillsDir: join(homedir(), '.hermes', 'skills'),
445
+ outputStylesDir: join(homedir(), '.hermes', 'output-styles'),
446
+ envVar: 'PEAKS_HERMES_SKILLS_DIR',
447
+ outputStylesEnvVar: 'PEAKS_HERMES_OUTPUT_STYLES_DIR',
448
+ },
449
+ 'openclaw': {
450
+ skillsDir: join(homedir(), '.openclaw', 'skills'),
451
+ outputStylesDir: join(homedir(), '.openclaw', 'output-styles'),
452
+ envVar: 'PEAKS_OPENCLAW_SKILLS_DIR',
453
+ outputStylesEnvVar: 'PEAKS_OPENCLAW_OUTPUT_STYLES_DIR',
454
+ },
404
455
  };
405
456
 
406
457
  function detectInstalledIdeId(projectRoot) {
@@ -657,9 +708,228 @@ export function installBundledOutputStyles(options = {}) {
657
708
  return { installed, skipped };
658
709
  }
659
710
 
711
+ /**
712
+ * Per-platform fan-out — iterate ALL 8 IdeIds and call
713
+ * `installBundledSkills` for each. Per peaks-cli tenet
714
+ * "minimal-user-operation" (2026-06-11): the user should
715
+ * never have to run a per-platform install command. The
716
+ * 1.x postinstall only handled the auto-detected single
717
+ * IDE; 2.0 fixes this so the peaks-* skill family is
718
+ * symlinked to every platform the user might be on.
719
+ *
720
+ * Returns an array of { ideId, skillsDir, installed, skipped }
721
+ * per platform. Symlink failures are soft (logged to stderr,
722
+ * never throw) so one platform's failure doesn't block the
723
+ * other 7.
724
+ */
725
+ export function installBundledSkillsForAllPlatforms(options = {}) {
726
+ const platforms = Object.keys(IDE_SKILL_INSTALL_PROFILES);
727
+ const perPlatform = [];
728
+ // Back-compat precedence (regression fix 2026-06-12,
729
+ // slice 2026-06-12-postinstall-1x-detector-tdd):
730
+ // when iterating the 8 platforms, the claude-code install
731
+ // must still honor the PEAKS_CLAUDE_SKILLS_DIR env var
732
+ // (the legacy back-compat surface from 1.x). The other 7
733
+ // platforms use their per-IDE profile paths unconditionally.
734
+ // Without this fix the 8-IDE fan-out regresses the
735
+ // `peaks install-skills` env-var override contract that
736
+ // user CI / 1.x → 2.0 migration scripts depend on.
737
+ const claudeEnv = process.env.PEAKS_CLAUDE_SKILLS_DIR;
738
+ for (const ideId of platforms) {
739
+ try {
740
+ const platformOpts =
741
+ ideId === 'claude-code' && claudeEnv !== undefined && claudeEnv.length > 0
742
+ ? { ...options, ideId, targetRoot: claudeEnv }
743
+ : { ...options, ideId };
744
+ const result = installBundledSkills(platformOpts);
745
+ perPlatform.push({
746
+ ideId,
747
+ skillsDir: IDE_SKILL_INSTALL_PROFILES[ideId]?.skillsDir ?? '(unknown)',
748
+ installed: result.installed,
749
+ skipped: result.skipped,
750
+ });
751
+ } catch (err) {
752
+ const message = err instanceof Error ? err.message : String(err);
753
+ process.stderr.write(
754
+ `peaks install-skills: ${ideId} platform failed (continuing): ${message}\n`
755
+ );
756
+ perPlatform.push({
757
+ ideId,
758
+ skillsDir: IDE_SKILL_INSTALL_PROFILES[ideId]?.skillsDir ?? '(unknown)',
759
+ installed: [],
760
+ skipped: [],
761
+ error: message,
762
+ });
763
+ }
764
+ }
765
+ return perPlatform;
766
+ }
767
+
768
+ /**
769
+ * 1.x → 2.0 detection — sniff for legacy 1.x project state
770
+ * in `cwd`. Returns a 1.x detection envelope with the
771
+ * detected signals (so the postinstall can decide whether
772
+ * to auto-upgrade).
773
+ *
774
+ * 1.x signals (any one fires the detection):
775
+ * - `~/.peaks/config.json` exists with `version: '1.4.2'` (or
776
+ * any '1.x' version that predates the 2.0 schema)
777
+ * - `.claude/rules/common/dev-preference.md` exists and
778
+ * references "peaks progress" (the 1.x CLI surface
779
+ * removed in slice #014)
780
+ * - `<cwd>/.peaks/preferences.json` missing OR has no
781
+ * `schema_version: '2.0.0'` field
782
+ *
783
+ * Returns:
784
+ * { isOneX: boolean, signals: string[], projectRoot: string|null,
785
+ * configPath: string|null }
786
+ */
787
+ export function detect1xProjectState(cwd = process.cwd()) {
788
+ const home = homedir();
789
+ const signals = [];
790
+ let projectRoot = null;
791
+ let configPath = null;
792
+
793
+ // Walk up from cwd looking for .peaks/_runtime (signals
794
+ // we're inside a peaks project).
795
+ let dir = cwd;
796
+ for (let i = 0; i < 8; i += 1) {
797
+ const peaksRuntime = join(dir, '.peaks', '_runtime');
798
+ if (existsSync(peaksRuntime)) {
799
+ projectRoot = dir;
800
+ break;
801
+ }
802
+ const parent = dirname(dir);
803
+ if (parent === dir) break;
804
+ dir = parent;
805
+ }
806
+
807
+ // Signal 1: ~/.peaks/config.json with 1.x version
808
+ const globalConfig = join(home, '.peaks', 'config.json');
809
+ if (existsSync(globalConfig)) {
810
+ try {
811
+ const raw = JSON.parse(readFileSync(globalConfig, 'utf8'));
812
+ if (typeof raw.version === 'string' && /^1\./.test(raw.version)) {
813
+ signals.push(`global config at ${globalConfig} is 1.x (${raw.version})`);
814
+ if (configPath === null) configPath = globalConfig;
815
+ }
816
+ } catch {
817
+ // ignore parse error — the 1.x detection is best-effort
818
+ }
819
+ }
820
+
821
+ // Signal 2: .claude/rules/common/dev-preference.md with peaks progress
822
+ if (projectRoot !== null) {
823
+ const devPref = join(projectRoot, '.claude', 'rules', 'common', 'dev-preference.md');
824
+ if (existsSync(devPref)) {
825
+ try {
826
+ const body = readFileSync(devPref, 'utf8');
827
+ if (/peaks progress/i.test(body)) {
828
+ signals.push(`${devPref} references "peaks progress" (1.x CLI surface, removed in slice #014)`);
829
+ }
830
+ } catch {
831
+ // ignore
832
+ }
833
+ }
834
+ // Signal 3: project preferences.json missing or 1.x
835
+ const prefs = join(projectRoot, '.peaks', 'preferences.json');
836
+ if (!existsSync(prefs)) {
837
+ signals.push(`${prefs} does not exist (1.x project never migrated)`);
838
+ } else {
839
+ try {
840
+ const raw = JSON.parse(readFileSync(prefs, 'utf8'));
841
+ if (raw.schema_version !== '2.0.0') {
842
+ signals.push(`${prefs} has schema_version ${JSON.stringify(raw.schema_version)}, expected '2.0.0'`);
843
+ }
844
+ } catch {
845
+ signals.push(`${prefs} exists but is not valid JSON`);
846
+ }
847
+ }
848
+ }
849
+
850
+ return {
851
+ isOneX: signals.length > 0,
852
+ signals,
853
+ projectRoot,
854
+ configPath,
855
+ };
856
+ }
857
+
858
+ /**
859
+ * Postinstall auto-upgrade — when the user just ran
860
+ * `npm i -g peaks-cli@2.0` and `cwd` is a 1.x peaks-cli
861
+ * project, this shells out to the installed `peaks`
862
+ * binary to run the umbrella `peaks upgrade --to 2.0 --auto`.
863
+ *
864
+ * Per the "minimal-user-operation" tenet, the user should
865
+ * never have to run a second command after `npm i -g`. The
866
+ * upgrade CLI (if installed) is at the resolved `peaks`
867
+ * binary path; if not, the user gets a hint to run it
868
+ * manually.
869
+ *
870
+ * The auto-upgrade is opt-out via:
871
+ * PEAKS_SKIP_AUTO_UPGRADE=1
872
+ * (so a CI box that installs 2.0 but never wants the
873
+ * project-level migration can suppress the auto-step).
874
+ */
875
+ export async function autoUpgrade1xProjectIfPresent(options = {}) {
876
+ if (process.env.PEAKS_SKIP_AUTO_UPGRADE === '1') {
877
+ return { ran: false, reason: 'PEAKS_SKIP_AUTO_UPGRADE=1' };
878
+ }
879
+ const state = detect1xProjectState(options.cwd ?? process.cwd());
880
+ if (!state.isOneX) {
881
+ return { ran: false, reason: 'no 1.x project state detected' };
882
+ }
883
+ if (state.projectRoot === null) {
884
+ return { ran: false, reason: 'cwd is not a peaks project (no .peaks/_runtime/)' };
885
+ }
886
+ // The peaks binary should be on PATH after `npm i -g`.
887
+ // We shell out via spawnSync (synchronous; the postinstall
888
+ // is already synchronous and the umbrella is fast).
889
+ try {
890
+ const result = spawnSync('peaks', ['upgrade', '--to', '2.0', '--auto', '--project', state.projectRoot], {
891
+ encoding: 'utf8',
892
+ stdio: ['ignore', 'pipe', 'pipe'],
893
+ timeout: 120_000,
894
+ });
895
+ return {
896
+ ran: true,
897
+ reason: 'auto-upgrade dispatched',
898
+ signals: state.signals,
899
+ projectRoot: state.projectRoot,
900
+ exitCode: result.status,
901
+ stdout: result.stdout ?? '',
902
+ stderr: result.stderr ?? '',
903
+ };
904
+ } catch (err) {
905
+ return {
906
+ ran: true,
907
+ reason: 'auto-upgrade dispatched but failed',
908
+ signals: state.signals,
909
+ projectRoot: state.projectRoot,
910
+ error: err instanceof Error ? err.message : String(err),
911
+ };
912
+ }
913
+ }
914
+
660
915
  if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
661
916
  try {
662
- const skillsResult = installBundledSkills();
917
+ // 2.0 fix for the 1.x Trae bug (per real user feedback
918
+ // 2026-06-11): iterate ALL 8 platforms, not just the
919
+ // auto-detected one. Per the "minimal-user-operation"
920
+ // tenet, the user should never have to run a
921
+ // per-platform install command.
922
+ const perPlatform = installBundledSkillsForAllPlatforms();
923
+ let totalInstalled = 0;
924
+ for (const p of perPlatform) {
925
+ totalInstalled += p.installed.length;
926
+ }
927
+ if (totalInstalled > 0) {
928
+ process.stdout.write(
929
+ `Peaks skills linked across ${perPlatform.length} platforms ` +
930
+ `(${totalInstalled} total symlinks)\n`
931
+ );
932
+ }
663
933
  const outputStylesResult = installBundledOutputStyles();
664
934
  let userConfigResult = createConfigResult({ skipped: true });
665
935
  try {
@@ -668,12 +938,6 @@ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(resolve(p
668
938
  const message = error instanceof Error ? error.message : String(error);
669
939
  process.stderr.write(`Peaks user config was not installed: ${message}\n`);
670
940
  }
671
- if (skillsResult.installed.length > 0) {
672
- process.stdout.write(`Peaks skills linked: ${skillsResult.installed.join(', ')}\n`);
673
- }
674
- if (skillsResult.skipped.length > 0) {
675
- process.stderr.write(`Peaks skills skipped because local files already exist: ${skillsResult.skipped.join(', ')}\n`);
676
- }
677
941
  if (outputStylesResult.installed.length > 0) {
678
942
  process.stdout.write(`Peaks output styles installed: ${outputStylesResult.installed.join(', ')}\n`);
679
943
  }
@@ -683,6 +947,26 @@ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(resolve(p
683
947
  if (userConfigResult.created) {
684
948
  process.stdout.write('Peaks user config created: ~/.peaks/config.json\n');
685
949
  }
950
+
951
+ // 2.0 postinstall: auto-detect 1.x project state in cwd
952
+ // and dispatch the upgrade umbrella. This makes the
953
+ // user's `npm i -g peaks-cli@2.0` truly one-key.
954
+ if (process.env.PEAKS_SKIP_AUTO_UPGRADE !== '1') {
955
+ // Fire-and-forget; the upgrade is async by design so
956
+ // the npm install output isn't blocked. We print a
957
+ // one-line hint so the user knows the auto-step
958
+ // happened.
959
+ autoUpgrade1xProjectIfPresent().then((result) => {
960
+ if (result.ran) {
961
+ process.stdout.write(
962
+ `\n✓ Detected 1.x peaks-cli project at ${result.projectRoot}\n` +
963
+ ` → auto-upgraded to 2.0 (${result.signals?.length ?? 0} signals resolved)\n` +
964
+ ` Run \`peaks audit red-lines --project .\` to verify.\n`
965
+ );
966
+ }
967
+ // When !result.ran we say nothing — silent on success.
968
+ });
969
+ }
686
970
  if (userConfigResult.updated) {
687
971
  process.stdout.write('Peaks user config updated: ~/.peaks/config.json\n');
688
972
  }