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,127 @@
1
+ /**
2
+ * Red-line classifier — turns raw markdown lines into RedLineEntry.
3
+ *
4
+ * Algorithm: for each MarkdownLine, check whether the line text contains one
5
+ * of the four red-line markers (MANDATORY / BLOCKING / MUST NOT / RED LINE).
6
+ * On a hit, extract the surrounding ±2 lines as context, look up the red
7
+ * line in the catalog, and emit a RedLineEntry. Lines that contain a marker
8
+ * but match no catalog entry still produce a RedLineEntry (with backing =
9
+ * prose-only and enforcerRef = null) — those are the "discovered but not yet
10
+ * enforced" red lines the L2 redesign is working to eliminate.
11
+ */
12
+ import { findCatalogEntry } from './red-line-catalog.js';
13
+ const MARKER_PATTERN = /\b(MANDATORY|BLOCKING|MUST NOT|RED LINE)\b/;
14
+ const CONTEXT_LINES_BEFORE = 2;
15
+ const CONTEXT_LINES_AFTER = 2;
16
+ function isMarker(s) {
17
+ return s === 'MANDATORY' || s === 'BLOCKING' || s === 'MUST NOT' || s === 'RED LINE';
18
+ }
19
+ function extractContext(allLines, hitLine) {
20
+ const start = Math.max(0, hitLine - CONTEXT_LINES_BEFORE);
21
+ const end = Math.min(allLines.length, hitLine + 1 + CONTEXT_LINES_AFTER);
22
+ return allLines.slice(start, end).join('\n');
23
+ }
24
+ /**
25
+ * Detect markers in a single line of markdown. Returns the marker text found
26
+ * (uppercased), or null when no marker is present.
27
+ */
28
+ /**
29
+ * Case-insensitive marker pattern (used only by detectMarker so callers can
30
+ * write `MANDATORY` / `mandatory` / `Mandatory` interchangeably in prose).
31
+ * deriveRuleName uses the all-caps MARKER_PATTERN so it doesn't strip a
32
+ * mid-sentence "Red Line" reference.
33
+ */
34
+ const MARKER_PATTERN_CI = /\b(MANDATORY|BLOCKING|MUST NOT|RED LINE)\b/i;
35
+ export function detectMarker(lineText) {
36
+ const match = MARKER_PATTERN_CI.exec(lineText);
37
+ if (!match)
38
+ return null;
39
+ const raw = (match[1] ?? '').toUpperCase();
40
+ if (raw === 'MUST') {
41
+ // "MUST NOT" is two tokens in the regex; the match group only captures
42
+ // "MUST". Re-verify by checking the next character.
43
+ const after = lineText[match.index + match[0].length];
44
+ if (after === undefined || /\s/.test(after)) {
45
+ return 'MUST NOT';
46
+ }
47
+ return isMarker(raw) ? raw : null;
48
+ }
49
+ return isMarker(raw) ? raw : null;
50
+ }
51
+ /**
52
+ * Derive a human-readable rule name from the marker line. Heuristic: take
53
+ * the first 8 words of the line, lowercase, trimmed. Catalog matching is
54
+ * the source of truth for the canonical rule name; this is the fallback
55
+ * when no catalog entry matches.
56
+ */
57
+ export function deriveRuleName(lineText) {
58
+ const cleaned = lineText
59
+ .replace(MARKER_PATTERN, '')
60
+ .replace(/^[*_`#>:]+/, '')
61
+ .replace(/[*_`#>]/g, '')
62
+ .trim();
63
+ const words = cleaned.split(/\s+/).filter(Boolean).slice(0, 8);
64
+ return words.length > 0 ? words.join(' ').toLowerCase() : 'unspecified red line';
65
+ }
66
+ /**
67
+ * Classify a single markdown file. Returns 0+ RedLineEntry; one entry per
68
+ * marker hit. Marker hits that appear multiple times on the same line are
69
+ * counted once.
70
+ */
71
+ export function classifyFile(input) {
72
+ const entries = [];
73
+ const warnings = [];
74
+ const seen = new Set();
75
+ for (let idx = 0; idx < input.lines.length; idx++) {
76
+ const lineText = input.lines[idx] ?? '';
77
+ if (seen.has(idx + 1))
78
+ continue;
79
+ const marker = detectMarker(lineText);
80
+ if (marker === null)
81
+ continue;
82
+ seen.add(idx + 1);
83
+ const context = extractContext(input.lines, idx);
84
+ const ruleName = deriveRuleName(lineText);
85
+ const markers = [marker];
86
+ const catalog = findCatalogEntry(ruleName, markers);
87
+ const source = {
88
+ file: input.file,
89
+ line: idx + 1,
90
+ marker,
91
+ context,
92
+ };
93
+ if (catalog) {
94
+ entries.push({
95
+ id: catalog.id,
96
+ rule: catalog.rule,
97
+ source,
98
+ backing: catalog.enforcerRef === null ? 'prose-only' : 'cli-backed',
99
+ enforcerRef: catalog.enforcerRef,
100
+ });
101
+ }
102
+ else {
103
+ // Marker hit but no catalog match: discovered, not yet enforced.
104
+ entries.push({
105
+ id: `rl-discovered-${input.file.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}-${idx + 1}`,
106
+ rule: ruleName,
107
+ source,
108
+ backing: 'prose-only',
109
+ enforcerRef: null,
110
+ });
111
+ }
112
+ }
113
+ return { entries, warnings };
114
+ }
115
+ /**
116
+ * Batch wrapper — classify each input file and flatten the entries.
117
+ */
118
+ export function classifyFiles(inputs) {
119
+ const allEntries = [];
120
+ const allWarnings = [];
121
+ for (const input of inputs) {
122
+ const result = classifyFile(input);
123
+ allEntries.push(...result.entries);
124
+ allWarnings.push(...result.warnings);
125
+ }
126
+ return { entries: allEntries, warnings: allWarnings };
127
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * active-skill-resolver — utility for hook enforcers.
3
+ *
4
+ * Resolves the active peak skill name for the current session, so hook
5
+ * enforcers (e.g. solo-code-ban) can decide whether to fire.
6
+ *
7
+ * Per `src/services/session/caller-id-types.ts`: the active-skill file is
8
+ * at `.peaks/_runtime/<peakSessionId>/active-skill-<callerId>.json`.
9
+ *
10
+ * Resolution order (graceful degradation — never throws):
11
+ * 1. PEAKS_ACTIVE_SKILL env var (explicit override, used by tests)
12
+ * 2. .peaks/_runtime/<sid>/active-skill-<callerId>.json for each caller
13
+ * bound to the current peak session
14
+ * 3. null (caller did not set a skill; enforcers can decide to skip)
15
+ */
16
+ export interface ActiveSkillResolution {
17
+ readonly skill: string | null;
18
+ readonly callerId: string | null;
19
+ readonly sessionId: string | null;
20
+ readonly source: 'env' | 'file' | 'none';
21
+ }
22
+ /**
23
+ * Resolve the active peak skill for the current hook invocation.
24
+ *
25
+ * Reads PEAKS_ACTIVE_SKILL first (test override), then walks
26
+ * `.peaks/_runtime/<sid>/` for any `active-skill-*.json` file. Returns
27
+ * the first match.
28
+ */
29
+ export declare function resolveActiveSkillForCaller(projectRoot: string): ActiveSkillResolution;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * active-skill-resolver — utility for hook enforcers.
3
+ *
4
+ * Resolves the active peak skill name for the current session, so hook
5
+ * enforcers (e.g. solo-code-ban) can decide whether to fire.
6
+ *
7
+ * Per `src/services/session/caller-id-types.ts`: the active-skill file is
8
+ * at `.peaks/_runtime/<peakSessionId>/active-skill-<callerId>.json`.
9
+ *
10
+ * Resolution order (graceful degradation — never throws):
11
+ * 1. PEAKS_ACTIVE_SKILL env var (explicit override, used by tests)
12
+ * 2. .peaks/_runtime/<sid>/active-skill-<callerId>.json for each caller
13
+ * bound to the current peak session
14
+ * 3. null (caller did not set a skill; enforcers can decide to skip)
15
+ */
16
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { getSessionIdCanonical } from '../../session/session-manager.js';
19
+ import { getSessionDir } from '../../session/getSessionDir.js';
20
+ const ACTIVE_SKILL_PREFIX = 'active-skill-';
21
+ /**
22
+ * Resolve the active peak skill for the current hook invocation.
23
+ *
24
+ * Reads PEAKS_ACTIVE_SKILL first (test override), then walks
25
+ * `.peaks/_runtime/<sid>/` for any `active-skill-*.json` file. Returns
26
+ * the first match.
27
+ */
28
+ export function resolveActiveSkillForCaller(projectRoot) {
29
+ const envOverride = process.env.PEAKS_ACTIVE_SKILL;
30
+ if (typeof envOverride === 'string' && envOverride.length > 0) {
31
+ return { skill: envOverride, callerId: null, sessionId: null, source: 'env' };
32
+ }
33
+ let sessionId = null;
34
+ try {
35
+ sessionId = getSessionIdCanonical(projectRoot);
36
+ }
37
+ catch {
38
+ return { skill: null, callerId: null, sessionId: null, source: 'none' };
39
+ }
40
+ if (sessionId === null) {
41
+ return { skill: null, callerId: null, sessionId: null, source: 'none' };
42
+ }
43
+ const sessionDir = getSessionDir(projectRoot, sessionId);
44
+ if (!existsSync(sessionDir)) {
45
+ return { skill: null, callerId: null, sessionId: sessionId, source: 'none' };
46
+ }
47
+ let entries;
48
+ try {
49
+ entries = readdirSync(sessionDir);
50
+ }
51
+ catch {
52
+ return { skill: null, callerId: null, sessionId: sessionId, source: 'none' };
53
+ }
54
+ for (const entry of entries) {
55
+ if (!entry.startsWith(ACTIVE_SKILL_PREFIX) || !entry.endsWith('.json'))
56
+ continue;
57
+ const callerId = entry.slice(ACTIVE_SKILL_PREFIX.length, -'.json'.length);
58
+ const filePath = join(sessionDir, entry);
59
+ try {
60
+ const raw = readFileSync(filePath, 'utf8');
61
+ const parsed = JSON.parse(raw);
62
+ if (typeof parsed.skill === 'string' && parsed.skill.length > 0) {
63
+ return { skill: parsed.skill, callerId, sessionId, source: 'file' };
64
+ }
65
+ }
66
+ catch {
67
+ // skip malformed file
68
+ }
69
+ }
70
+ return { skill: null, callerId: null, sessionId, source: 'none' };
71
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * design-draft-confirm enforcer (L2.2 P1) — verifies a design draft exists
3
+ * and has been confirmed before the rd implementation phase begins.
4
+ *
5
+ * Two red lines:
6
+ * - rl-design-draft-confirm-001: design-draft.md must exist before spec-locked
7
+ * - rl-design-draft-confirm-002: design-draft must have a 'confirmed' marker
8
+ *
9
+ * The state-machine check is wired into peaks request transition (the
10
+ * spec-locked transition requires both files to exist + design to be
11
+ * confirmed). The catalog flags the entry as cli-backed when the
12
+ * enforcer file exists.
13
+ */
14
+ export interface DesignDraftConfirmInput {
15
+ readonly projectRoot: string;
16
+ readonly sessionId: string;
17
+ readonly changeId: string;
18
+ }
19
+ export interface DesignDraftConfirmResult {
20
+ readonly draftExists: boolean;
21
+ readonly draftPath: string;
22
+ readonly confirmed: boolean;
23
+ readonly confirmationPath: string;
24
+ }
25
+ export declare function checkDesignDraftConfirmation(input: DesignDraftConfirmInput): DesignDraftConfirmResult;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * design-draft-confirm enforcer (L2.2 P1) — verifies a design draft exists
3
+ * and has been confirmed before the rd implementation phase begins.
4
+ *
5
+ * Two red lines:
6
+ * - rl-design-draft-confirm-001: design-draft.md must exist before spec-locked
7
+ * - rl-design-draft-confirm-002: design-draft must have a 'confirmed' marker
8
+ *
9
+ * The state-machine check is wired into peaks request transition (the
10
+ * spec-locked transition requires both files to exist + design to be
11
+ * confirmed). The catalog flags the entry as cli-backed when the
12
+ * enforcer file exists.
13
+ */
14
+ import { existsSync, readFileSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ const CONFIRMATION_MARKERS = [
17
+ /\bconfirmed\b\s*[:=]\s*true/i,
18
+ /\bstatus:\s*confirmed-by-user/i,
19
+ /^#\s*confirmed\b/im,
20
+ ];
21
+ export function checkDesignDraftConfirmation(input) {
22
+ // Design drafts live at .peaks/<changeId>/ui/design-draft.md (UI role) or
23
+ // .peaks/<changeId>/prd/requests/<rid>.md (PRD). For L2.2 the canonical
24
+ // location is the UI design-draft.
25
+ const draftPath = join(input.projectRoot, '.peaks', input.changeId, 'ui/design-draft.md');
26
+ const draftExists = existsSync(draftPath);
27
+ if (!draftExists) {
28
+ return {
29
+ draftExists: false,
30
+ draftPath,
31
+ confirmed: false,
32
+ confirmationPath: '',
33
+ };
34
+ }
35
+ let content;
36
+ try {
37
+ content = readFileSync(draftPath, 'utf8');
38
+ }
39
+ catch {
40
+ return {
41
+ draftExists: true,
42
+ draftPath,
43
+ confirmed: false,
44
+ confirmationPath: draftPath,
45
+ };
46
+ }
47
+ const confirmed = CONFIRMATION_MARKERS.some((p) => p.test(content));
48
+ return {
49
+ draftExists: true,
50
+ draftPath,
51
+ confirmed,
52
+ confirmationPath: draftPath,
53
+ };
54
+ }
@@ -0,0 +1,21 @@
1
+ import type { LintHit } from './lint-style.js';
2
+ export declare const CATALOG_STABILITY_GROWTH_CAP = 0.2;
3
+ export declare const CATALOG_STABILITY_WINDOW_DAYS = 90;
4
+ export declare const RUNTIME_BUDGET_MS = 2000;
5
+ export interface CatalogStabilityInput {
6
+ /** Current catalog size (entries.length). */
7
+ readonly currentSize: number;
8
+ /** Catalog size 90 days ago, or null if no history available. */
9
+ readonly sizeNinetyDaysAgo: number | null;
10
+ }
11
+ export declare function lintCatalogStability(input: CatalogStabilityInput): readonly LintHit[];
12
+ export declare function lintNoOrphanEnforcer(projectRoot: string): readonly LintHit[];
13
+ export declare function lintNoOrphanCatalog(): readonly LintHit[];
14
+ export declare function lintRuntimeBudget(projectRoot: string, observedMs: number): readonly LintHit[];
15
+ /**
16
+ * Read the catalog-stability history file (if it exists). The
17
+ * file is a small JSON document maintained by the release
18
+ * pipeline; absent → null. We do not invent the historical
19
+ * data; absent data means "soft pass".
20
+ */
21
+ export declare function readCatalogHistory(projectRoot: string): number | null;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * P2-b Theme L — Audit regression enforcers.
3
+ *
4
+ * Slice #7 L2.4. These four enforcers run during the audit
5
+ * and check the audit framework's own integrity. They are
6
+ * the "gating" layer that `peaks slice check`'s new
7
+ * audit-regression stage asserts.
8
+ *
9
+ * Four checks:
10
+ * 1. catalog-stability — catalog size has not grown >20% in 90 days
11
+ * 2. no-orphan-enforcer — every enforcerRef points to a real file
12
+ * 3. no-orphan-catalog — every catalog entry has a non-null enforcerRef
13
+ * 4. runtime-budget — audit completes in < 2s on a 100-reference project
14
+ */
15
+ import { existsSync, readFileSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import { RED_LINE_CATALOG } from '../red-line-catalog.js';
18
+ export const CATALOG_STABILITY_GROWTH_CAP = 0.20;
19
+ export const CATALOG_STABILITY_WINDOW_DAYS = 90;
20
+ export const RUNTIME_BUDGET_MS = 2000;
21
+ const fakeCatalogPath = 'src/services/audit/red-line-catalog.ts';
22
+ function syntheticHit(catalogId, rule, detail) {
23
+ return {
24
+ catalogId,
25
+ rule,
26
+ file: fakeCatalogPath,
27
+ line: 1,
28
+ matchedText: detail,
29
+ };
30
+ }
31
+ export function lintCatalogStability(input) {
32
+ if (input.sizeNinetyDaysAgo === null || input.sizeNinetyDaysAgo === 0) {
33
+ // No historical data — can't check stability. Treat as a
34
+ // soft pass (the catalog-stability enforcer is
35
+ // non-blocking when the data is missing).
36
+ return [];
37
+ }
38
+ const growth = (input.currentSize - input.sizeNinetyDaysAgo) / input.sizeNinetyDaysAgo;
39
+ if (growth <= CATALOG_STABILITY_GROWTH_CAP)
40
+ return [];
41
+ return [syntheticHit('rl-audit-catalog-stability-001', 'catalog size has not grown > 20% in the last 90 days', `(growth ${(growth * 100).toFixed(1)}% over 90 days; currentSize=${input.currentSize}, priorSize=${input.sizeNinetyDaysAgo})`)];
42
+ }
43
+ export function lintNoOrphanEnforcer(projectRoot) {
44
+ const hits = [];
45
+ for (const entry of RED_LINE_CATALOG) {
46
+ if (!entry.enforcerRef)
47
+ continue;
48
+ const absPath = join(projectRoot, entry.enforcerRef);
49
+ if (!existsSync(absPath)) {
50
+ hits.push(syntheticHit('rl-audit-no-orphan-enforcer-001', 'every enforcerRef points to a real file', `(enforcerRef "${entry.enforcerRef}" for ${entry.id} does not exist on disk)`));
51
+ }
52
+ }
53
+ return hits;
54
+ }
55
+ export function lintNoOrphanCatalog() {
56
+ const hits = [];
57
+ for (const entry of RED_LINE_CATALOG) {
58
+ if (entry.enforcerRef === null) {
59
+ hits.push(syntheticHit('rl-audit-no-orphan-catalog-001', 'every catalog entry has a non-null enforcerRef (or a documented reason)', `(catalog entry ${entry.id} has enforcerRef: null)`));
60
+ }
61
+ }
62
+ return hits;
63
+ }
64
+ export function lintRuntimeBudget(projectRoot, observedMs) {
65
+ if (observedMs <= RUNTIME_BUDGET_MS)
66
+ return [];
67
+ return [syntheticHit('rl-audit-runtime-budget-001', `peaks audit red-lines completes in < ${RUNTIME_BUDGET_MS}ms on a 100-reference project`, `(observed ${observedMs}ms > budget ${RUNTIME_BUDGET_MS}ms)`)];
68
+ }
69
+ /**
70
+ * Read the catalog-stability history file (if it exists). The
71
+ * file is a small JSON document maintained by the release
72
+ * pipeline; absent → null. We do not invent the historical
73
+ * data; absent data means "soft pass".
74
+ */
75
+ export function readCatalogHistory(projectRoot) {
76
+ const path = join(projectRoot, '.peaks', 'audit-catalog-history.json');
77
+ if (!existsSync(path))
78
+ return null;
79
+ try {
80
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
81
+ return typeof raw.sizeNinetyDaysAgo === 'number' ? raw.sizeNinetyDaysAgo : null;
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * P2-a Theme G — catalog governance.
3
+ *
4
+ * Two enforcers: catalog size must grow to ≥ 40 (the P2-a target),
5
+ * and the prose-only ratio must stay ≤ 5% (per spec §10.2 L2
6
+ * acceptance). Both fire on the catalog's static state — no file
7
+ * scan, just the catalog itself.
8
+ */
9
+ import type { LintHit } from './lint-style.js';
10
+ export declare const CATALOG_SIZE_TARGET = 40;
11
+ export declare const PROSE_ONLY_RATIO_TARGET = 0.05;
12
+ export interface CatalogSize {
13
+ readonly size: number;
14
+ readonly target: number;
15
+ }
16
+ export interface CatalogProseOnlyRatio {
17
+ readonly ratio: number;
18
+ readonly target: number;
19
+ }
20
+ export declare function lintCatalogSize(actualSize: number): readonly LintHit[];
21
+ /**
22
+ * Prose-only ratio: count catalog entries whose `enforcerRef` is
23
+ * null (i.e. not backed by a CLI surface) divided by the total
24
+ * catalog size. Per spec §10.2, the L2 acceptance is ≤ 10% at
25
+ * P2-a; this slice tightens to ≤ 5%.
26
+ */
27
+ export declare function lintCatalogProseOnlyRatio(catalogSize: number, proseOnlyCount: number): readonly LintHit[];
@@ -0,0 +1,38 @@
1
+ export const CATALOG_SIZE_TARGET = 40;
2
+ export const PROSE_ONLY_RATIO_TARGET = 0.05;
3
+ function syntheticHit(catalogId, rule, matched) {
4
+ // No specific file to point at — return a synthetic hit against
5
+ // the catalog source file so the audit report can render a row.
6
+ const fake = {
7
+ name: 'catalog',
8
+ path: 'src/services/audit/red-line-catalog.ts',
9
+ body: '',
10
+ lines: [],
11
+ };
12
+ return {
13
+ catalogId,
14
+ rule,
15
+ file: fake.path,
16
+ line: 1,
17
+ matchedText: matched,
18
+ };
19
+ }
20
+ export function lintCatalogSize(actualSize) {
21
+ if (actualSize >= CATALOG_SIZE_TARGET)
22
+ return [];
23
+ return [syntheticHit('rl-catalog-total-001', 'Catalog governance: catalog size must grow to ≥ 40 (L2.3 P2-a target)', `(catalog size ${actualSize} < target ${CATALOG_SIZE_TARGET})`)];
24
+ }
25
+ /**
26
+ * Prose-only ratio: count catalog entries whose `enforcerRef` is
27
+ * null (i.e. not backed by a CLI surface) divided by the total
28
+ * catalog size. Per spec §10.2, the L2 acceptance is ≤ 10% at
29
+ * P2-a; this slice tightens to ≤ 5%.
30
+ */
31
+ export function lintCatalogProseOnlyRatio(catalogSize, proseOnlyCount) {
32
+ if (catalogSize === 0)
33
+ return [];
34
+ const ratio = proseOnlyCount / catalogSize;
35
+ if (ratio <= PROSE_ONLY_RATIO_TARGET)
36
+ return [];
37
+ return [syntheticHit('rl-catalog-prose-only-ratio-001', 'Catalog governance: prose-only ratio must stay ≤ 5% (per §10.2 L2 acceptance)', `(prose-only ratio ${(ratio * 100).toFixed(1)}% > target ${PROSE_ONLY_RATIO_TARGET * 100}%; ${proseOnlyCount}/${catalogSize})`)];
38
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * P2-a Theme D — CLI-back gaps enforcers.
3
+ *
4
+ * A red line that says "BLOCKING" / "MANDATORY" / "MUST NOT" / "MUST"
5
+ * / "REQUIRED" must point at a `peaks *` enforcer in the
6
+ * surrounding ±2 lines, or it stays as `prose-only` (per §5.2).
7
+ *
8
+ * The enforcer walks each skill body, locates every
9
+ * `MANDATORY` / `BLOCKING` / `MUST NOT` marker, and reports an
10
+ * orphan-hit when the surrounding text does NOT name a peaks CLI
11
+ * command.
12
+ */
13
+ import type { LintHit, SkillFile } from './lint-style.js';
14
+ export declare function lintCliBackMandatorText(skill: SkillFile): readonly LintHit[];
15
+ export declare function lintCliBackNoOrphanBlocking(skill: SkillFile): readonly LintHit[];
16
+ export declare function lintCliBackNoOrphanMustNot(skill: SkillFile): readonly LintHit[];
@@ -0,0 +1,35 @@
1
+ const MANDATORY_PATTERN = /\bMANDATORY\b/;
2
+ const BLOCKING_PATTERN = /\bBLOCKING\b/;
3
+ const MUST_NOT_PATTERN = /\bMUST NOT\b/;
4
+ const PEAKS_CLI_PATTERN = /\bpeaks\s+[a-z][a-z0-9-]*/;
5
+ function findOrphans(skill, pattern, catalogId, rule) {
6
+ const hits = [];
7
+ for (let i = 0; i < skill.lines.length; i += 1) {
8
+ const line = skill.lines[i] ?? '';
9
+ if (!pattern.test(line))
10
+ continue;
11
+ // Look at ±2 lines for a `peaks *` reference
12
+ const ctx = skill.lines
13
+ .slice(Math.max(0, i - 2), Math.min(skill.lines.length, i + 3))
14
+ .join('\n');
15
+ if (!PEAKS_CLI_PATTERN.test(ctx)) {
16
+ hits.push({
17
+ catalogId,
18
+ rule,
19
+ file: skill.path,
20
+ line: i + 1,
21
+ matchedText: line.trim()
22
+ });
23
+ }
24
+ }
25
+ return hits;
26
+ }
27
+ export function lintCliBackMandatorText(skill) {
28
+ return findOrphans(skill, MANDATORY_PATTERN, 'rl-cli-back-mandatory-text-001', 'MANDATORY text has peaks * enforcer in the surrounding ±2 lines');
29
+ }
30
+ export function lintCliBackNoOrphanBlocking(skill) {
31
+ return findOrphans(skill, BLOCKING_PATTERN, 'rl-cli-back-no-orphan-blocking-001', 'no orphan BLOCKING marker without a peaks * enforcer');
32
+ }
33
+ export function lintCliBackNoOrphanMustNot(skill) {
34
+ return findOrphans(skill, MUST_NOT_PATTERN, 'rl-cli-back-no-orphan-must-not-001', 'no orphan MUST NOT marker without a peaks * enforcer');
35
+ }
@@ -0,0 +1,11 @@
1
+ import type { LintHit, SkillFile } from './lint-style.js';
2
+ export declare function lintNoFluff(skill: SkillFile): readonly LintHit[];
3
+ export declare function lintNoClosingPrompt(skill: SkillFile): readonly LintHit[];
4
+ /**
5
+ * Status header detection: scan the most-recent session log for the
6
+ * canonical header line. The session log is read from the project's
7
+ * `.peaks/_runtime/<sid>/session.log` (or the audit's per-test
8
+ * fixture). For static analysis (no session), the lint returns an
9
+ * empty array (the runtime check is elsewhere).
10
+ */
11
+ export declare function lintStatusHeader(projectRoot: string, sessionId: string): readonly LintHit[];
@@ -0,0 +1,94 @@
1
+ /**
2
+ * P2-a Theme C — output style enforcers.
3
+ *
4
+ * The peaks-cli dogfood rule: every skill response carries a status
5
+ * header (`Peaks-Cli Skill: <name> | Peaks-Cli Gate: <gate> | Next:
6
+ * <action>`), the SKILL.md body has no greeting/closing-prompt
7
+ * flattery, and the status header is detectable in the recent
8
+ * session transcript (test-only fixture).
9
+ *
10
+ * The status-header detection is a real check on the fixture's
11
+ * most-recent session file; the no-fluff and no-closing-prompt
12
+ * checks are static pattern scans of the skill's SKILL.md.
13
+ */
14
+ import { readFileSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ const FLUFF_GREETINGS = [
17
+ /\b你好[!!。,,.]?\s*$/m,
18
+ /^\s*你好[!!。,,.]?\s*$/m,
19
+ /\bHello[,,]?\s*I am\b/i,
20
+ /\bI am (an? )?(helpful )?assistant\b/i,
21
+ /\b作为一个 AI\b/,
22
+ /\b我是 (一个 )?AI\b/,
23
+ /\b作为一个\b/,
24
+ /\b我是(助手|模型|AI)/i
25
+ ];
26
+ const CLOSING_PROMPTS = [
27
+ /\bLet me know if you need\b/i,
28
+ /\b如有(任何)?需要\b/,
29
+ /\b如有需要\b/,
30
+ /\bfeel free to ask\b/i,
31
+ /\bdo not hesitate to\b/i,
32
+ /\b随时(欢迎)?(联系|提问)/i,
33
+ /\b随时 (欢迎 )?(联系|提问)/i
34
+ ];
35
+ const STATUS_HEADER_PATTERN = /Peaks-Cli Skill:\s*peaks-[a-z0-9-]+\s*\|\s*Peaks-Cli Gate:\s*[A-Za-z0-9_.-]+\s*\|\s*Next:\s*\S/;
36
+ export function lintNoFluff(skill) {
37
+ const hits = [];
38
+ for (const pattern of FLUFF_GREETINGS) {
39
+ const lineIdx = skill.lines.findIndex((l) => pattern.test(l));
40
+ if (lineIdx !== -1) {
41
+ hits.push({
42
+ catalogId: 'rl-output-style-no-fluff-001',
43
+ rule: 'no greeting / persona fluff in SKILL.md',
44
+ file: skill.path,
45
+ line: lineIdx + 1,
46
+ matchedText: (skill.lines[lineIdx] ?? '').trim()
47
+ });
48
+ }
49
+ }
50
+ return hits;
51
+ }
52
+ export function lintNoClosingPrompt(skill) {
53
+ const hits = [];
54
+ for (const pattern of CLOSING_PROMPTS) {
55
+ const lineIdx = skill.lines.findIndex((l) => pattern.test(l));
56
+ if (lineIdx !== -1) {
57
+ hits.push({
58
+ catalogId: 'rl-output-style-no-closing-prompt-001',
59
+ rule: 'no closing-prompt flattery',
60
+ file: skill.path,
61
+ line: lineIdx + 1,
62
+ matchedText: (skill.lines[lineIdx] ?? '').trim()
63
+ });
64
+ }
65
+ }
66
+ return hits;
67
+ }
68
+ /**
69
+ * Status header detection: scan the most-recent session log for the
70
+ * canonical header line. The session log is read from the project's
71
+ * `.peaks/_runtime/<sid>/session.log` (or the audit's per-test
72
+ * fixture). For static analysis (no session), the lint returns an
73
+ * empty array (the runtime check is elsewhere).
74
+ */
75
+ export function lintStatusHeader(projectRoot, sessionId) {
76
+ const logPath = join(projectRoot, '.peaks', '_runtime', sessionId, 'session.log');
77
+ let body;
78
+ try {
79
+ body = readFileSync(logPath, 'utf8');
80
+ }
81
+ catch {
82
+ return [];
83
+ }
84
+ if (!STATUS_HEADER_PATTERN.test(body)) {
85
+ return [{
86
+ catalogId: 'rl-output-style-status-header-001',
87
+ rule: 'Peaks-Cli status header on every response',
88
+ file: logPath,
89
+ line: 1,
90
+ matchedText: '(no Peaks-Cli Skill / Gate / Next header found in session.log)'
91
+ }];
92
+ }
93
+ return [];
94
+ }
@@ -0,0 +1,6 @@
1
+ import type { LintHit, SkillFile } from './lint-style.js';
2
+ /** Theme E — reference integrity. */
3
+ export declare function lintRefPathResolves(skillsRoot: string, name: string, refs: readonly string[]): readonly LintHit[];
4
+ export declare function lintNoBrokenMkdir(skill: SkillFile): readonly LintHit[];
5
+ export declare function lintNoPwdSymlinkJumps(skill: SkillFile): readonly LintHit[];
6
+ export declare function lintNoRelativeArchivePaths(skill: SkillFile): readonly LintHit[];