peaks-cli 1.4.2 → 2.0.1

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 (180) hide show
  1. package/.claude-plugin/marketplace.json +51 -0
  2. package/CHANGELOG.md +279 -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/capability-commands.js +2 -1
  10. package/dist/src/cli/commands/classify-classify-commands.d.ts +19 -0
  11. package/dist/src/cli/commands/classify-classify-commands.js +151 -0
  12. package/dist/src/cli/commands/code-review-commands.d.ts +34 -0
  13. package/dist/src/cli/commands/code-review-commands.js +83 -0
  14. package/dist/src/cli/commands/config-commands.js +90 -0
  15. package/dist/src/cli/commands/context-commands.d.ts +21 -0
  16. package/dist/src/cli/commands/context-commands.js +167 -0
  17. package/dist/src/cli/commands/core-artifact-commands.js +60 -2
  18. package/dist/src/cli/commands/hook-handle.js +50 -0
  19. package/dist/src/cli/commands/loop-commands.d.ts +21 -0
  20. package/dist/src/cli/commands/loop-commands.js +128 -0
  21. package/dist/src/cli/commands/openspec-commands.js +37 -0
  22. package/dist/src/cli/commands/preferences-commands.d.ts +2 -0
  23. package/dist/src/cli/commands/preferences-commands.js +147 -0
  24. package/dist/src/cli/commands/skill-conformance-commands.d.ts +9 -0
  25. package/dist/src/cli/commands/skill-conformance-commands.js +39 -0
  26. package/dist/src/cli/commands/understand-commands.js +34 -0
  27. package/dist/src/cli/commands/upgrade-commands.d.ts +23 -0
  28. package/dist/src/cli/commands/upgrade-commands.js +57 -0
  29. package/dist/src/cli/commands/workflow-commands.js +70 -0
  30. package/dist/src/cli/commands/workspace-commands.js +117 -2
  31. package/dist/src/cli/program.js +30 -0
  32. package/dist/src/lib/render/message-renderer.d.ts +20 -0
  33. package/dist/src/lib/render/message-renderer.js +80 -0
  34. package/dist/src/services/agent/ecc-agent-service.d.ts +47 -0
  35. package/dist/src/services/agent/ecc-agent-service.js +143 -0
  36. package/dist/src/services/artifacts/request-artifact-service.js +14 -0
  37. package/dist/src/services/audit/backing-detector.d.ts +24 -0
  38. package/dist/src/services/audit/backing-detector.js +59 -0
  39. package/dist/src/services/audit/classifier.d.ts +38 -0
  40. package/dist/src/services/audit/classifier.js +127 -0
  41. package/dist/src/services/audit/enforcers/active-skill-resolver.d.ts +29 -0
  42. package/dist/src/services/audit/enforcers/active-skill-resolver.js +71 -0
  43. package/dist/src/services/audit/enforcers/design-draft-confirm.d.ts +25 -0
  44. package/dist/src/services/audit/enforcers/design-draft-confirm.js +54 -0
  45. package/dist/src/services/audit/enforcers/lint-audit-regression.d.ts +21 -0
  46. package/dist/src/services/audit/enforcers/lint-audit-regression.js +86 -0
  47. package/dist/src/services/audit/enforcers/lint-catalog-governance.d.ts +27 -0
  48. package/dist/src/services/audit/enforcers/lint-catalog-governance.js +38 -0
  49. package/dist/src/services/audit/enforcers/lint-cli-back.d.ts +16 -0
  50. package/dist/src/services/audit/enforcers/lint-cli-back.js +35 -0
  51. package/dist/src/services/audit/enforcers/lint-output-style.d.ts +11 -0
  52. package/dist/src/services/audit/enforcers/lint-output-style.js +94 -0
  53. package/dist/src/services/audit/enforcers/lint-reference-integrity.d.ts +6 -0
  54. package/dist/src/services/audit/enforcers/lint-reference-integrity.js +83 -0
  55. package/dist/src/services/audit/enforcers/lint-reference-shape.d.ts +30 -0
  56. package/dist/src/services/audit/enforcers/lint-reference-shape.js +272 -0
  57. package/dist/src/services/audit/enforcers/lint-style.d.ts +49 -0
  58. package/dist/src/services/audit/enforcers/lint-style.js +173 -0
  59. package/dist/src/services/audit/enforcers/lint-workflow-shape.d.ts +5 -0
  60. package/dist/src/services/audit/enforcers/lint-workflow-shape.js +141 -0
  61. package/dist/src/services/audit/enforcers/login-gate.d.ts +23 -0
  62. package/dist/src/services/audit/enforcers/login-gate.js +40 -0
  63. package/dist/src/services/audit/enforcers/mock-placement.d.ts +25 -0
  64. package/dist/src/services/audit/enforcers/mock-placement.js +48 -0
  65. package/dist/src/services/audit/enforcers/no-root-pollution.d.ts +21 -0
  66. package/dist/src/services/audit/enforcers/no-root-pollution.js +56 -0
  67. package/dist/src/services/audit/enforcers/pre-rd-scan.d.ts +22 -0
  68. package/dist/src/services/audit/enforcers/pre-rd-scan.js +23 -0
  69. package/dist/src/services/audit/enforcers/prototype-fidelity.d.ts +25 -0
  70. package/dist/src/services/audit/enforcers/prototype-fidelity.js +75 -0
  71. package/dist/src/services/audit/enforcers/resume-detection.d.ts +21 -0
  72. package/dist/src/services/audit/enforcers/resume-detection.js +52 -0
  73. package/dist/src/services/audit/enforcers/solo-code-ban.d.ts +23 -0
  74. package/dist/src/services/audit/enforcers/solo-code-ban.js +27 -0
  75. package/dist/src/services/audit/enforcers/sub-agent-sid.d.ts +25 -0
  76. package/dist/src/services/audit/enforcers/sub-agent-sid.js +63 -0
  77. package/dist/src/services/audit/enforcers/tech-doc-presence.d.ts +28 -0
  78. package/dist/src/services/audit/enforcers/tech-doc-presence.js +35 -0
  79. package/dist/src/services/audit/red-line-catalog-p2-a.d.ts +21 -0
  80. package/dist/src/services/audit/red-line-catalog-p2-a.js +233 -0
  81. package/dist/src/services/audit/red-line-catalog-p2-b.d.ts +19 -0
  82. package/dist/src/services/audit/red-line-catalog-p2-b.js +225 -0
  83. package/dist/src/services/audit/red-line-catalog.d.ts +51 -0
  84. package/dist/src/services/audit/red-line-catalog.js +210 -0
  85. package/dist/src/services/audit/red-lines-service.d.ts +23 -0
  86. package/dist/src/services/audit/red-lines-service.js +486 -0
  87. package/dist/src/services/audit/scanners/openspec-scanner.d.ts +15 -0
  88. package/dist/src/services/audit/scanners/openspec-scanner.js +55 -0
  89. package/dist/src/services/audit/scanners/rules-tree-scanner.d.ts +16 -0
  90. package/dist/src/services/audit/scanners/rules-tree-scanner.js +56 -0
  91. package/dist/src/services/audit/scanners/skills-tree-scanner.d.ts +17 -0
  92. package/dist/src/services/audit/scanners/skills-tree-scanner.js +46 -0
  93. package/dist/src/services/audit/static-service.d.ts +57 -0
  94. package/dist/src/services/audit/static-service.js +125 -0
  95. package/dist/src/services/audit/types.d.ts +69 -0
  96. package/dist/src/services/audit/types.js +13 -0
  97. package/dist/src/services/classify/classify-service.d.ts +42 -0
  98. package/dist/src/services/classify/classify-service.js +122 -0
  99. package/dist/src/services/classify/classify-types.d.ts +79 -0
  100. package/dist/src/services/classify/classify-types.js +90 -0
  101. package/dist/src/services/code-review/ocr-service.d.ts +129 -0
  102. package/dist/src/services/code-review/ocr-service.js +362 -0
  103. package/dist/src/services/config/config-migration.d.ts +32 -0
  104. package/dist/src/services/config/config-migration.js +111 -0
  105. package/dist/src/services/config/config-restore.d.ts +10 -0
  106. package/dist/src/services/config/config-restore.js +47 -0
  107. package/dist/src/services/config/config-rollback.d.ts +13 -0
  108. package/dist/src/services/config/config-rollback.js +26 -0
  109. package/dist/src/services/config/config-service.d.ts +36 -2
  110. package/dist/src/services/config/config-service.js +105 -0
  111. package/dist/src/services/config/config-types.d.ts +73 -0
  112. package/dist/src/services/config/config-types.js +28 -13
  113. package/dist/src/services/config/model-routing.js +5 -3
  114. package/dist/src/services/doctor/doctor-service.js +96 -0
  115. package/dist/src/services/ide/adapters/hermes-adapter.d.ts +21 -0
  116. package/dist/src/services/ide/adapters/hermes-adapter.js +51 -0
  117. package/dist/src/services/ide/adapters/openclaw-adapter.d.ts +14 -0
  118. package/dist/src/services/ide/adapters/openclaw-adapter.js +42 -0
  119. package/dist/src/services/ide/ide-registry.js +7 -0
  120. package/dist/src/services/ide/ide-types.d.ts +1 -1
  121. package/dist/src/services/openspec/openspec-propose-from-doctor-service.d.ts +31 -0
  122. package/dist/src/services/openspec/openspec-propose-from-doctor-service.js +95 -0
  123. package/dist/src/services/preferences/preferences-service.d.ts +6 -0
  124. package/dist/src/services/preferences/preferences-service.js +43 -0
  125. package/dist/src/services/preferences/preferences-types.d.ts +90 -0
  126. package/dist/src/services/preferences/preferences-types.js +38 -0
  127. package/dist/src/services/rd/rd-service.js +29 -1
  128. package/dist/src/services/skills/skill-conformance-service.d.ts +40 -0
  129. package/dist/src/services/skills/skill-conformance-service.js +136 -0
  130. package/dist/src/services/skills/skill-runbook-service.js +44 -10
  131. package/dist/src/services/skills/sync-service.d.ts +86 -0
  132. package/dist/src/services/skills/sync-service.js +271 -0
  133. package/dist/src/services/slice/slice-check-service.js +166 -13
  134. package/dist/src/services/slice/slice-check-types.d.ts +1 -1
  135. package/dist/src/services/standards/migrate-claude-rules-service.d.ts +19 -0
  136. package/dist/src/services/standards/migrate-claude-rules-service.js +193 -0
  137. package/dist/src/services/understand/understand-scan-service.js +15 -2
  138. package/dist/src/services/understand/understand-types.d.ts +26 -0
  139. package/dist/src/services/upgrade/1x-detector-service.d.ts +7 -0
  140. package/dist/src/services/upgrade/1x-detector-service.js +94 -0
  141. package/dist/src/services/upgrade/gitignore-migrate-service.d.ts +56 -0
  142. package/dist/src/services/upgrade/gitignore-migrate-service.js +170 -0
  143. package/dist/src/services/upgrade/upgrade-service.d.ts +47 -0
  144. package/dist/src/services/upgrade/upgrade-service.js +381 -0
  145. package/dist/src/services/workflow/workflow-router-service.js +15 -4
  146. package/dist/src/services/workspace/claude-settings-template.d.ts +53 -0
  147. package/dist/src/services/workspace/claude-settings-template.js +133 -0
  148. package/dist/src/services/workspace/sid-naming-guard.d.ts +14 -0
  149. package/dist/src/services/workspace/sid-naming-guard.js +31 -0
  150. package/dist/src/services/workspace/workspace-archive-service.d.ts +19 -0
  151. package/dist/src/services/workspace/workspace-archive-service.js +32 -0
  152. package/dist/src/services/workspace/workspace-clean-service.d.ts +41 -0
  153. package/dist/src/services/workspace/workspace-clean-service.js +86 -0
  154. package/dist/src/services/workspace/workspace-service.d.ts +24 -0
  155. package/dist/src/services/workspace/workspace-service.js +124 -2
  156. package/dist/src/services/workspace/workspace-state-service.d.ts +7 -0
  157. package/dist/src/services/workspace/workspace-state-service.js +43 -0
  158. package/dist/src/shared/change-id.js +4 -1
  159. package/dist/src/shared/version.d.ts +1 -1
  160. package/dist/src/shared/version.js +1 -1
  161. package/package.json +8 -2
  162. package/schemas/doctor-report.schema.json +1 -1
  163. package/scripts/install-skills.mjs +296 -12
  164. package/skills/peaks-doctor/SKILL.md +59 -0
  165. package/skills/peaks-doctor/references/doctor-check-catalog.md +31 -0
  166. package/skills/peaks-doctor/references/from-doctor-flow.md +64 -0
  167. package/skills/peaks-doctor/test_prompts.json +17 -0
  168. package/skills/peaks-ide/SKILL.md +2 -0
  169. package/skills/peaks-qa/SKILL.md +9 -7
  170. package/skills/peaks-qa/references/artifact-per-request.md +19 -5
  171. package/skills/peaks-qa/references/qa-perf-test-plan.md +6 -6
  172. package/skills/peaks-qa/references/qa-runbook.md +1 -1
  173. package/skills/peaks-rd/SKILL.md +25 -10
  174. package/skills/peaks-rd/references/ocr-integration.md +214 -0
  175. package/skills/peaks-rd/references/rd-fanout-contracts.md +70 -0
  176. package/skills/peaks-rd/references/rd-runbook.md +1 -1
  177. package/skills/peaks-solo/SKILL.md +16 -4
  178. package/skills/peaks-solo/references/anchoring-and-session-info.md +9 -0
  179. package/skills/peaks-solo/references/step-0-55-1x-detection.md +82 -0
  180. package/skills/peaks-solo/references/workflow-gates-and-types.md +9 -0
@@ -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
+ }
@@ -17,6 +17,14 @@ export type WorkspaceInitOptions = {
17
17
  * (live sub-agent progress, spawn records).
18
18
  */
19
19
  changeId?: string;
20
+ /**
21
+ * Slice 2.0.1-bug3-fact-forcing-bypass: opt out of writing the
22
+ * consumer-project `.claude/settings.local.json` file. Default
23
+ * (`false`) writes the file so the [Fact-Forcing Gate] is bypassed
24
+ * for tool calls inside `.peaks/**`. The CLI surfaces this as
25
+ * `--no-claude-hooks`.
26
+ */
27
+ noClaudeHooks?: boolean;
20
28
  };
21
29
  export type WorkspaceInitReport = {
22
30
  sessionId: string;
@@ -27,6 +35,22 @@ export type WorkspaceInitReport = {
27
35
  previousSessionId: string | null;
28
36
  changeId: string | null;
29
37
  changeIdAction: 'bound' | 'preserved' | 'none';
38
+ /**
39
+ * Slice 2.0.1-bug3-fact-forcing-bypass: what the consumer-project
40
+ * `.claude/settings.local.json` materialization did this call.
41
+ * - written: the file was freshly written
42
+ * - refreshed: the file already existed and was rewritten to
43
+ * match the current peaks-cli release's template
44
+ * - already-current: the file already matched the template; no
45
+ * rewrite needed
46
+ * - skipped: the caller passed noClaudeHooks=true
47
+ * The LLM and the user both see this in the JSON envelope so they
48
+ * can decide whether the bypass is in effect.
49
+ */
50
+ claudeSettings: {
51
+ action: 'written' | 'refreshed' | 'already-current' | 'skipped';
52
+ path: string;
53
+ };
30
54
  };
31
55
  export declare class InvalidSessionIdError extends Error {
32
56
  readonly code = "INVALID_SESSION_ID";
@@ -1,8 +1,10 @@
1
- import { mkdir } from 'node:fs/promises';
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
2
3
  import { join } from 'node:path';
3
4
  import { isDirectory } from '../../shared/fs.js';
4
5
  import { getSessionId, setCurrentSessionBinding, setSessionMeta } from '../session/session-manager.js';
5
6
  import { setCurrentChangeId } from '../../shared/change-id.js';
7
+ import { buildClaudeSettingsLocalJson, CLAUDE_SETTINGS_LOCAL_FILENAME } from './claude-settings-template.js';
6
8
  const SESSION_ID_PATTERN = /^\d{4}-\d{2}-\d{2}-[a-z][a-z0-9-]*[a-z0-9]$/;
7
9
  const PROHIBITED_SUFFIXES = ['session', 'work', 'task', 'test', 'temp', 'tmp'];
8
10
  // Auto-generated session ID pattern: YYYY-MM-DD-session-<6位hex>
@@ -173,6 +175,126 @@ export async function initWorkspace(options) {
173
175
  bound,
174
176
  previousSessionId,
175
177
  changeId: resolvedChangeId,
176
- changeIdAction
178
+ changeIdAction,
179
+ claudeSettings: await materializeClaudeSettingsLocal(options.projectRoot, options.noClaudeHooks === true)
177
180
  };
178
181
  }
182
+ /**
183
+ * The peaks-managed snippet appended to the consumer project's
184
+ * `.peaks/.gitignore` so the local-only settings file never lands
185
+ * in a commit. Marked with a managed-by header so we can detect (and
186
+ * not double-append) on subsequent inits.
187
+ */
188
+ const PEAKS_GITIGNORE_HEADER = '# >>> peaks-cli managed snippet (slice 2.0.1-bug3) — do not edit by hand';
189
+ const PEAKS_GITIGNORE_FOOTER = '# <<< peaks-cli managed snippet';
190
+ const PEAKS_GITIGNORE_SNIPPET = [
191
+ PEAKS_GITIGNORE_HEADER,
192
+ '# Consumer-project .claude/settings.local.json: written by `peaks workspace init`',
193
+ '# to bypass Claude Code [Fact-Forcing Gate] for .peaks/** writes. Local-only.',
194
+ '.claude/settings.local.json',
195
+ PEAKS_GITIGNORE_FOOTER,
196
+ ''
197
+ ].join('\n');
198
+ /**
199
+ * Materialize the consumer-project `.claude/settings.local.json` and
200
+ * ensure the consumer's `.peaks/.gitignore` covers it. Returns a
201
+ * `claudeSettings` descriptor that the caller surfaces in the JSON
202
+ * envelope.
203
+ *
204
+ * The function is idempotent: re-running on an already-materialized
205
+ * project is a no-op (the file is rewritten only when its content
206
+ * diverges from the current peaks-cli release's template, which
207
+ * keeps the consumer up to date as the template evolves).
208
+ *
209
+ * Even when the caller passes `noClaudeHooks: true`, the function
210
+ * still writes a copy of the template at
211
+ * `.peaks/.claude-settings-template.json` so the user has an offline
212
+ * recovery path: copy the file contents into
213
+ * `.claude/settings.local.json` manually. The recovery path is
214
+ * documented in
215
+ * `skills/peaks-solo/references/anchoring-and-session-info.md`.
216
+ */
217
+ async function materializeClaudeSettingsLocal(projectRoot, noClaudeHooks) {
218
+ const settingsRel = CLAUDE_SETTINGS_LOCAL_FILENAME;
219
+ const settingsPath = join(projectRoot, settingsRel);
220
+ const template = buildClaudeSettingsLocalJson();
221
+ const serialized = JSON.stringify(template, null, 2) + '\n';
222
+ // Always drop a copy of the template under .peaks/ so the
223
+ // --no-claude-hooks recovery flow has a known source-of-truth on
224
+ // disk. The file is gitignored by the snippet below.
225
+ await writeOfflineTemplateCopy(projectRoot, serialized);
226
+ if (noClaudeHooks) {
227
+ return { action: 'skipped', path: settingsRel };
228
+ }
229
+ // Best-effort: ensure .claude/ exists, then write the file. We do
230
+ // not assertSafeSettingsPath here (the .claude/ dir is local to
231
+ // the consumer and we trust it on first init; the existing
232
+ // hooks-settings-service applies the safety check for the Bash
233
+ // gate-enforce path).
234
+ await mkdir(join(projectRoot, '.claude'), { recursive: true });
235
+ let action = 'written';
236
+ if (existsSync(settingsPath)) {
237
+ try {
238
+ const { readFile } = await import('node:fs/promises');
239
+ const existing = await readFile(settingsPath, 'utf8');
240
+ if (existing === serialized) {
241
+ action = 'already-current';
242
+ }
243
+ else {
244
+ action = 'refreshed';
245
+ }
246
+ }
247
+ catch {
248
+ // Treat any read failure as "needs refresh" so the consumer
249
+ // always ends up with a valid template on disk.
250
+ action = 'refreshed';
251
+ }
252
+ }
253
+ if (action !== 'already-current') {
254
+ await writeFile(settingsPath, serialized, 'utf8');
255
+ }
256
+ // Ensure the consumer's .peaks/.gitignore covers the local-only
257
+ // settings file. The snippet is appended only when the header is
258
+ // missing, so subsequent inits do not double-append.
259
+ await upsertPeaksGitignoreSnippet(projectRoot);
260
+ return { action, path: settingsRel };
261
+ }
262
+ /**
263
+ * Always write (or refresh) a copy of the template at
264
+ * `.peaks/.claude-settings-template.json` so the user has a known
265
+ * source-of-truth on disk for the manual recovery flow. This file is
266
+ * tracked in git (not gitignored) because it is the recovery anchor
267
+ * — if the consumer needs to re-create their .claude/settings.local.json
268
+ * they can copy this file verbatim.
269
+ */
270
+ async function writeOfflineTemplateCopy(projectRoot, serialized) {
271
+ const copyPath = join(projectRoot, '.peaks', '.claude-settings-template.json');
272
+ await mkdir(join(projectRoot, '.peaks'), { recursive: true });
273
+ await writeFile(copyPath, serialized, 'utf8');
274
+ }
275
+ /**
276
+ * Append the peaks-managed `.claude/settings.local.json` snippet to
277
+ * the consumer project's `.peaks/.gitignore`. Preserves any user-
278
+ * managed entries above the snippet. Idempotent: re-running on a
279
+ * project that already has the snippet is a no-op.
280
+ */
281
+ async function upsertPeaksGitignoreSnippet(projectRoot) {
282
+ const gitignorePath = join(projectRoot, '.peaks', '.gitignore');
283
+ await mkdir(join(projectRoot, '.peaks'), { recursive: true });
284
+ let existing = '';
285
+ if (existsSync(gitignorePath)) {
286
+ try {
287
+ const { readFile } = await import('node:fs/promises');
288
+ existing = await readFile(gitignorePath, 'utf8');
289
+ }
290
+ catch {
291
+ existing = '';
292
+ }
293
+ }
294
+ if (existing.includes(PEAKS_GITIGNORE_HEADER)) {
295
+ return;
296
+ }
297
+ const separator = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
298
+ const next = existing + separator + (existing.length > 0 ? '\n' : '') + PEAKS_GITIGNORE_SNIPPET;
299
+ await writeFile(gitignorePath, next, 'utf8');
300
+ }
@@ -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.1";
@@ -1 +1 @@
1
- export const CLI_VERSION = "1.4.2";
1
+ export const CLI_VERSION = "2.0.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-cli",
3
- "version": "1.4.2",
3
+ "version": "2.0.1",
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" },