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,173 @@
1
+ /**
2
+ * P2-a lint-style enforcers — Theme A (section structure) and
3
+ * Theme B (frontmatter shape).
4
+ *
5
+ * These enforcers do NOT block commands; they feed the
6
+ * `peaks audit red-lines --json` report. The backing-detector
7
+ * downgrades them to `cli-backed` when the catalog has an
8
+ * `enforcerRef` (i.e. this file) and the per-skill scan finds a
9
+ * match. The audit service counts hits + misses and reports the
10
+ * per-catalog-entry breakdown.
11
+ *
12
+ * Pattern-only (no shell-out, no FS writes, no env mutations) so
13
+ * the enforcer can run inside the `peaks audit red-lines` hot
14
+ * path without the 5s ECC-AgentShield subprocess budget.
15
+ */
16
+ import { readFileSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ const SECTION_HARD_CONTRACTS_HEADING = /^##\s+(Hard contracts|Hard contract)\b/im;
19
+ const SECTION_MANDATORY_HEADING = /^##\s+Mandatory\b/im;
20
+ const SECTION_DEFAULT_RUNBOOK_HEADING = /^##\s+(Default runbook|Default)\b/im;
21
+ const SECTION_GATE_INDEX_HEADING = /^##\s+(RD gate index|QA gate index|Gate index|gate-index)\b/im;
22
+ const SECTION_NAMING_AXIOM_HEADING = /(Two-axis naming convention|change-id.*session-id|두 가지 직교 축)/i;
23
+ const FRONTMATTER_NAME_LINE = /^name:\s*peaks-/m;
24
+ const FRONTMATTER_DESCRIPTION_LINE = /^description:\s*\S/m;
25
+ const FRONTMATTER_LOAD_STRATEGY = /loadStrategy:\s*(always|on-demand)/i;
26
+ const FRONTMATTER_APPLICABLE_TASK_LEVELS = /applicableTaskLevels/i;
27
+ export function readSkillFiles(skillsRoot, names) {
28
+ const out = [];
29
+ for (const name of names) {
30
+ const path = join(skillsRoot, name, 'SKILL.md');
31
+ const body = readFileSync(path, 'utf8');
32
+ out.push({ name, path, body, lines: body.split(/\r?\n/) });
33
+ }
34
+ return out;
35
+ }
36
+ function findLine(lines, pattern) {
37
+ for (let i = 0; i < lines.length; i += 1) {
38
+ if (pattern.test(lines[i] ?? ''))
39
+ return i + 1;
40
+ }
41
+ return -1;
42
+ }
43
+ function matchedText(lines, line) {
44
+ if (line <= 0)
45
+ return '';
46
+ return (lines[line - 1] ?? '').trim();
47
+ }
48
+ /** Theme A — section structure. Returns lint hits (positive = rule
49
+ * satisfied, so a missing heading fires the lint hit; downstream
50
+ * audit service decides whether to WARN or pass). */
51
+ export function lintSectionShape(skill) {
52
+ const hits = [];
53
+ const rules = [
54
+ { id: 'rl-section-hard-contracts-001', rule: 'Hard contracts for browser/IO surface', pattern: SECTION_HARD_CONTRACTS_HEADING },
55
+ { id: 'rl-section-mandatory-artifact-001', rule: 'Mandatory per-request artifact', pattern: SECTION_MANDATORY_HEADING },
56
+ { id: 'rl-section-default-runbook-001', rule: 'Default runbook pointer', pattern: SECTION_DEFAULT_RUNBOOK_HEADING },
57
+ { id: 'rl-section-gate-index-001', rule: 'Gate index', pattern: SECTION_GATE_INDEX_HEADING },
58
+ { id: 'rl-section-naming-axiom-001', rule: 'Two-axis naming axiom', pattern: SECTION_NAMING_AXIOM_HEADING }
59
+ ];
60
+ for (const r of rules) {
61
+ const line = findLine(skill.lines, r.pattern);
62
+ if (line === -1) {
63
+ hits.push({
64
+ catalogId: r.id,
65
+ rule: r.rule,
66
+ file: skill.path,
67
+ line: 1,
68
+ matchedText: '(missing section)'
69
+ });
70
+ }
71
+ }
72
+ return hits;
73
+ }
74
+ /**
75
+ * ASCII wireframe section-order check (spec §5.4 line 647).
76
+ *
77
+ * The "ASCII wireframe" lint hint in the spec means: SKILL.md
78
+ * follows a canonical section ordering. The first 5 sections
79
+ * must appear in this fixed order:
80
+ *
81
+ * 1. Two-axis naming axiom (top-of-file naming convention callout)
82
+ * 2. Hard contracts for browser/IO surface (when present)
83
+ * 3. Mandatory per-request artifact
84
+ * 4. Default runbook
85
+ * 5. RD / QA gate index
86
+ *
87
+ * If both sections exist but appear in the wrong order, the
88
+ * enforcer reports a wireframe-violation hit pointing at the
89
+ * section that came too early.
90
+ *
91
+ * If a section is missing, the per-section enforcer (above)
92
+ * already fires; this one only handles the **order** invariant
93
+ * (the wireframe shape).
94
+ */
95
+ export function lintSectionOrder(skill) {
96
+ const hits = [];
97
+ const order = [
98
+ { id: 'rl-section-naming-axiom-001', rule: 'Two-axis naming axiom must precede Hard contracts', pattern: SECTION_NAMING_AXIOM_HEADING },
99
+ { id: 'rl-section-hard-contracts-001', rule: 'Hard contracts must precede Default runbook', pattern: SECTION_HARD_CONTRACTS_HEADING },
100
+ { id: 'rl-section-default-runbook-001', rule: 'Default runbook must precede Gate index', pattern: SECTION_DEFAULT_RUNBOOK_HEADING },
101
+ ];
102
+ let lastSeenLine = -1;
103
+ let lastSeenRule = '';
104
+ for (const r of order) {
105
+ const line = findLine(skill.lines, r.pattern);
106
+ if (line === -1)
107
+ continue; // missing-section is its own enforcer
108
+ if (line < lastSeenLine) {
109
+ hits.push({
110
+ catalogId: 'rl-section-order-wireframe-001',
111
+ rule: 'Section wireframe order: each section must appear in the canonical order',
112
+ file: skill.path,
113
+ line,
114
+ matchedText: `(section "${r.rule}" at line ${line} comes after "${lastSeenRule}" at line ${lastSeenLine})`,
115
+ });
116
+ }
117
+ lastSeenLine = line;
118
+ lastSeenRule = r.rule;
119
+ }
120
+ return hits;
121
+ }
122
+ /** Theme B — frontmatter shape. Same convention as Theme A. */
123
+ export function lintFrontmatterShape(skill) {
124
+ const hits = [];
125
+ const hasName = FRONTMATTER_NAME_LINE.test(skill.body);
126
+ const hasDescription = FRONTMATTER_DESCRIPTION_LINE.test(skill.body);
127
+ if (!hasName || !hasDescription) {
128
+ hits.push({
129
+ catalogId: 'rl-frontmatter-skills-md-001',
130
+ rule: 'skills_md parseable frontmatter',
131
+ file: skill.path,
132
+ line: 1,
133
+ matchedText: '(missing name or description)'
134
+ });
135
+ }
136
+ // The `applicableTaskLevels` field is a body-level annotation, not
137
+ // strictly YAML frontmatter. We accept either a top-of-file
138
+ // frontmatter `applicableTaskLevels:` line or an in-body "applies
139
+ // to <levels>" sentence.
140
+ if (!FRONTMATTER_APPLICABLE_TASK_LEVELS.test(skill.body)) {
141
+ hits.push({
142
+ catalogId: 'rl-frontmatter-applicable-task-levels-001',
143
+ rule: 'skill applicable task levels',
144
+ file: skill.path,
145
+ line: 1,
146
+ matchedText: '(missing applicableTaskLevels)'
147
+ });
148
+ }
149
+ return hits;
150
+ }
151
+ /**
152
+ * Reference loadStrategy: scan every `references/*.md` in the
153
+ * skill's references/ dir for a `loadStrategy: always | on-demand`
154
+ * line. The audit is invoked per-skill, so this helper takes a
155
+ * `referencesRoot` (the skill's `references/` dir).
156
+ */
157
+ export function lintReferenceLoadStrategy(referencesRoot, refs) {
158
+ const hits = [];
159
+ for (const ref of refs) {
160
+ const path = join(referencesRoot, ref);
161
+ const body = readFileSync(path, 'utf8');
162
+ if (!FRONTMATTER_LOAD_STRATEGY.test(body)) {
163
+ hits.push({
164
+ catalogId: 'rl-frontmatter-references-load-strategy-001',
165
+ rule: 'references loadStrategy declared',
166
+ file: path,
167
+ line: 1,
168
+ matchedText: '(missing loadStrategy)'
169
+ });
170
+ }
171
+ }
172
+ return hits;
173
+ }
@@ -0,0 +1,5 @@
1
+ import type { LintHit, SkillFile } from './lint-style.js';
2
+ export declare function lintOpenSpecAcceptanceBullets(projectRoot: string): readonly LintHit[];
3
+ export declare function lintOpenSpecSpecReference(projectRoot: string): readonly LintHit[];
4
+ export declare function lintTechDocPresenceShape(projectRoot: string): readonly LintHit[];
5
+ export declare function lintPeaksDoctorAcknowledged(skill: SkillFile): readonly LintHit[];
@@ -0,0 +1,141 @@
1
+ /**
2
+ * P2-a Theme F - workflow-bound shape enforcers.
3
+ *
4
+ * Static pattern scans of openspec/changes/STAR/proposal.md, the
5
+ * rd/tech-doc.md template, and the request-artifact-writing skills
6
+ * for canonical shape (acceptance bullets, spec reference,
7
+ * red-line scope, peaks-doctor acknowledgement).
8
+ */
9
+ import { existsSync, lstatSync, readdirSync, readFileSync, statSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ const AC_BULLET_PATTERN = /^\s*-\s+\S/m;
12
+ const SPEC_REFERENCE_HEADING = /^##\s+Spec reference(?:\s+\(canonical\))?\s*$/im;
13
+ const RED_LINE_SCOPE_HEADING = /^##\s+Red-line scope\s*$/im;
14
+ const IMPL_EVIDENCE_HEADING = /^##\s+Implementation evidence\s*$/im;
15
+ const PEAKS_DOCTOR_PATTERN = /\bpeaks[- ]doctor\b/i;
16
+ export function lintOpenSpecAcceptanceBullets(projectRoot) {
17
+ const openspecDir = join(projectRoot, 'openspec', 'changes');
18
+ if (!existsSync(openspecDir))
19
+ return [];
20
+ const hits = [];
21
+ for (const entry of readdirSync(openspecDir)) {
22
+ const stat = statSync(join(openspecDir, entry));
23
+ if (!stat.isDirectory())
24
+ continue;
25
+ const proposal = join(openspecDir, entry, 'proposal.md');
26
+ if (!existsSync(proposal))
27
+ continue;
28
+ const body = readFileSync(proposal, 'utf8');
29
+ // Slice ## Acceptance Criteria heading.
30
+ const acMatch = /##\s+Acceptance Criteria\s*$/im.exec(body);
31
+ if (acMatch === null) {
32
+ hits.push({
33
+ catalogId: 'rl-openspec-proposal-has-acceptance-bullets-001',
34
+ rule: 'openspec proposal has non-empty Acceptance Criteria bullets',
35
+ file: proposal,
36
+ line: 1,
37
+ matchedText: '(missing ## Acceptance Criteria heading)'
38
+ });
39
+ continue;
40
+ }
41
+ // Capture everything from ## Acceptance Criteria up to the next ## heading.
42
+ const tail = body.slice(acMatch.index + acMatch[0].length);
43
+ const nextHeading = tail.search(/^##\s/m);
44
+ const acBlock = nextHeading === -1 ? tail : tail.slice(0, nextHeading);
45
+ if (!AC_BULLET_PATTERN.test(acBlock)) {
46
+ hits.push({
47
+ catalogId: 'rl-openspec-proposal-has-acceptance-bullets-001',
48
+ rule: 'openspec proposal has non-empty Acceptance Criteria bullets',
49
+ file: proposal,
50
+ line: 1,
51
+ matchedText: '(no `- ` bullet under ## Acceptance Criteria)'
52
+ });
53
+ }
54
+ }
55
+ return hits;
56
+ }
57
+ export function lintOpenSpecSpecReference(projectRoot) {
58
+ const openspecDir = join(projectRoot, 'openspec', 'changes');
59
+ if (!existsSync(openspecDir))
60
+ return [];
61
+ const hits = [];
62
+ for (const entry of readdirSync(openspecDir)) {
63
+ const stat = statSync(join(openspecDir, entry));
64
+ if (!stat.isDirectory())
65
+ continue;
66
+ const proposal = join(openspecDir, entry, 'proposal.md');
67
+ if (!existsSync(proposal))
68
+ continue;
69
+ const body = readFileSync(proposal, 'utf8');
70
+ if (!SPEC_REFERENCE_HEADING.test(body)) {
71
+ hits.push({
72
+ catalogId: 'rl-openspec-proposal-has-spec-changes-001',
73
+ rule: 'openspec proposal has Spec reference (canonical) link',
74
+ file: proposal,
75
+ line: 1,
76
+ matchedText: '(missing ## Spec reference (canonical) heading)'
77
+ });
78
+ }
79
+ }
80
+ return hits;
81
+ }
82
+ export function lintTechDocPresenceShape(projectRoot) {
83
+ // Scan every session's rd/tech-doc.md for the two required
84
+ // sections. The session dir is .peaks/_runtime/<sid>/.
85
+ // .peaks/_runtime/ also contains marker files like
86
+ // `active-skill.json`, `classify-audit.jsonl`, and a
87
+ // `current-change` symlink (per slice 2026-06-12-postinstall-
88
+ // 1x-detector-tdd, the current-change symlink can be broken
89
+ // across major session boundaries). Use lstatSync (does not
90
+ // follow symlinks) + isSymbolicLink guard so broken symlinks
91
+ // and marker files are skipped, not crashed on.
92
+ const runtimeDir = join(projectRoot, '.peaks', '_runtime');
93
+ if (!existsSync(runtimeDir))
94
+ return [];
95
+ const hits = [];
96
+ for (const sessionEntry of readdirSync(runtimeDir)) {
97
+ const sessionDir = join(runtimeDir, sessionEntry);
98
+ let stat;
99
+ try {
100
+ stat = lstatSync(sessionDir);
101
+ }
102
+ catch {
103
+ // broken symlink or unreadable entry — skip
104
+ continue;
105
+ }
106
+ if (stat.isSymbolicLink() || !stat.isDirectory())
107
+ continue;
108
+ const techDoc = join(sessionDir, 'rd', 'tech-doc.md');
109
+ if (!existsSync(techDoc))
110
+ continue;
111
+ const body = readFileSync(techDoc, 'utf8');
112
+ if (!RED_LINE_SCOPE_HEADING.test(body) || !IMPL_EVIDENCE_HEADING.test(body)) {
113
+ hits.push({
114
+ catalogId: 'rl-tech-doc-presence-pre-rd-001',
115
+ rule: 'rd/tech-doc.md has Red-line scope + Implementation evidence',
116
+ file: techDoc,
117
+ line: 1,
118
+ matchedText: '(missing ## Red-line scope or ## Implementation evidence)'
119
+ });
120
+ }
121
+ }
122
+ return hits;
123
+ }
124
+ export function lintPeaksDoctorAcknowledged(skill) {
125
+ // The enforcer fires only for skills that write a request
126
+ // artifact (rd / qa / prd / ui / sc / txt) — detected by the
127
+ // presence of `peaks request init` or `peaks request show` in
128
+ // the body.
129
+ const writesRequestArtifact = /\bpeaks\s+request\s+(init|show|transition)\b/.test(skill.body);
130
+ if (!writesRequestArtifact)
131
+ return [];
132
+ if (PEAKS_DOCTOR_PATTERN.test(skill.body))
133
+ return [];
134
+ return [{
135
+ catalogId: 'rl-peaks-doctor-skill-acknowledged-001',
136
+ rule: 'skill that writes a request artifact acknowledges peaks doctor',
137
+ file: skill.path,
138
+ line: 1,
139
+ matchedText: '(no peaks doctor / peaks-doctor mention in skill body)'
140
+ }];
141
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * login-gate enforcer (L2.2 P1) — verifies destructive / auth-required
3
+ * paths require explicit user confirmation.
4
+ *
5
+ * Two red lines:
6
+ * - rl-login-gate-001: destructive paths (uninstall, drop, force-push) must
7
+ * require user confirmation
8
+ * - rl-login-gate-002: protected paths (auth-required) must check session
9
+ *
10
+ * This is a static-check enforcer: the actual runtime gate is in
11
+ * `peaks mode-enforcement` (the requireUserConfirmation function). The
12
+ * catalog entry points to this file; the audit flags it as cli-backed
13
+ * when the integration is wired (L2.2 ships the wiring).
14
+ */
15
+ export interface LoginGateInput {
16
+ readonly command: string;
17
+ }
18
+ export interface LoginGateResult {
19
+ readonly destructive: boolean;
20
+ readonly protected: boolean;
21
+ readonly matchedPattern: string | null;
22
+ }
23
+ export declare function checkLoginGate(input: LoginGateInput): LoginGateResult;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * login-gate enforcer (L2.2 P1) — verifies destructive / auth-required
3
+ * paths require explicit user confirmation.
4
+ *
5
+ * Two red lines:
6
+ * - rl-login-gate-001: destructive paths (uninstall, drop, force-push) must
7
+ * require user confirmation
8
+ * - rl-login-gate-002: protected paths (auth-required) must check session
9
+ *
10
+ * This is a static-check enforcer: the actual runtime gate is in
11
+ * `peaks mode-enforcement` (the requireUserConfirmation function). The
12
+ * catalog entry points to this file; the audit flags it as cli-backed
13
+ * when the integration is wired (L2.2 ships the wiring).
14
+ */
15
+ const DESTRUCTIVE_PATH_PATTERNS = [
16
+ /uninstall/i,
17
+ /\bdrop\b/i,
18
+ /force-push/i,
19
+ /--force\b/,
20
+ /--hard\b/,
21
+ /rm\s+-rf?\b/,
22
+ ];
23
+ const PROTECTED_PATH_PATTERNS = [
24
+ /auth/i,
25
+ /login/i,
26
+ /session/i,
27
+ ];
28
+ export function checkLoginGate(input) {
29
+ for (const pattern of DESTRUCTIVE_PATH_PATTERNS) {
30
+ if (pattern.test(input.command)) {
31
+ return { destructive: true, protected: false, matchedPattern: pattern.source };
32
+ }
33
+ }
34
+ for (const pattern of PROTECTED_PATH_PATTERNS) {
35
+ if (pattern.test(input.command)) {
36
+ return { destructive: false, protected: true, matchedPattern: pattern.source };
37
+ }
38
+ }
39
+ return { destructive: false, protected: false, matchedPattern: null };
40
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * mock-placement enforcer — scans changed files for inline mock-data
3
+ * patterns and fails the slice check if any changed file under `src/` or
4
+ * `skills/` contains `mockData: { ... }`, `fixtures = { ... }`, or a
5
+ * multi-line `const fooMock = { ... }` literal.
6
+ *
7
+ * Per L2 redesign §5.4. Mocks belong in `tests/fixtures/`, not inline.
8
+ * Per `references/mock-data-placement.md` from peaks-rd: framework-aware
9
+ * mock placement. peaks-cli has no UI framework, but the rule still
10
+ * applies for non-fixture files.
11
+ */
12
+ export interface MockPlacementCheckInput {
13
+ readonly filePath: string;
14
+ readonly content: string;
15
+ }
16
+ export interface MockPlacementViolation {
17
+ readonly filePath: string;
18
+ readonly pattern: string;
19
+ readonly snippet: string;
20
+ }
21
+ export declare function hasInlineMock(content: string): MockPlacementViolation | null;
22
+ export declare function findMockViolations(changedFiles: readonly {
23
+ filePath: string;
24
+ content: string;
25
+ }[]): readonly MockPlacementViolation[];
@@ -0,0 +1,48 @@
1
+ /**
2
+ * mock-placement enforcer — scans changed files for inline mock-data
3
+ * patterns and fails the slice check if any changed file under `src/` or
4
+ * `skills/` contains `mockData: { ... }`, `fixtures = { ... }`, or a
5
+ * multi-line `const fooMock = { ... }` literal.
6
+ *
7
+ * Per L2 redesign §5.4. Mocks belong in `tests/fixtures/`, not inline.
8
+ * Per `references/mock-data-placement.md` from peaks-rd: framework-aware
9
+ * mock placement. peaks-cli has no UI framework, but the rule still
10
+ * applies for non-fixture files.
11
+ */
12
+ const MOCK_PATTERNS = [
13
+ /\bmockData\s*[:=]\s*\{/,
14
+ /\bfixtures?\s*=\s*\{/,
15
+ /const\s+\w*[Mm]ock\w*\s*=\s*\{[\s\S]{20,}/,
16
+ ];
17
+ export function hasInlineMock(content) {
18
+ for (const pattern of MOCK_PATTERNS) {
19
+ const match = pattern.exec(content);
20
+ if (match) {
21
+ return {
22
+ filePath: '',
23
+ pattern: pattern.source,
24
+ snippet: match[0].slice(0, 80),
25
+ };
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+ export function findMockViolations(changedFiles) {
31
+ const violations = [];
32
+ for (const { filePath, content } of changedFiles) {
33
+ // Mocks are allowed in tests/fixtures/. The slice check is invoked
34
+ // with the diff-vs-scope output, which already filters out
35
+ // test/fixture paths; this guard is a safety net.
36
+ if (filePath.includes('tests/fixtures/'))
37
+ continue;
38
+ if (filePath.includes('__fixtures__'))
39
+ continue;
40
+ if (!filePath.startsWith('src/') && !filePath.startsWith('skills/'))
41
+ continue;
42
+ const violation = hasInlineMock(content);
43
+ if (violation) {
44
+ violations.push({ ...violation, filePath });
45
+ }
46
+ }
47
+ return violations;
48
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * no-root-pollution enforcer — PreToolUse Write/Edit guard.
3
+ *
4
+ * Per L2 redesign §5.4. Deny writes to <project>/root for files NOT in
5
+ * the documented allowlist. The allowlist is hand-maintained for v1; a
6
+ * follow-up slice can expose it via `peaks standards`.
7
+ *
8
+ * Trust red line: this hook MUST fail-open on registry / FS errors. The
9
+ * LLM is never bricked by a peaks bug.
10
+ */
11
+ export interface RootWriteCheckInput {
12
+ readonly projectRoot: string;
13
+ readonly filePath: string;
14
+ }
15
+ export interface RootWriteCheckResult {
16
+ readonly isRoot: boolean;
17
+ readonly allowed: boolean;
18
+ readonly topSegment: string;
19
+ readonly denyReason: string;
20
+ }
21
+ export declare function isRootWrite(input: RootWriteCheckInput): RootWriteCheckResult;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * no-root-pollution enforcer — PreToolUse Write/Edit guard.
3
+ *
4
+ * Per L2 redesign §5.4. Deny writes to <project>/root for files NOT in
5
+ * the documented allowlist. The allowlist is hand-maintained for v1; a
6
+ * follow-up slice can expose it via `peaks standards`.
7
+ *
8
+ * Trust red line: this hook MUST fail-open on registry / FS errors. The
9
+ * LLM is never bricked by a peaks bug.
10
+ */
11
+ import { resolve } from 'node:path';
12
+ const ROOT_FILE_ALLOWLIST = new Set([
13
+ // Top-level docs
14
+ 'README.md', 'README-en.md', 'LICENSE', 'LICENSE.md', 'NOTICE', 'CONTRIBUTING.md',
15
+ 'CHANGELOG.md', 'AUTHORS', 'CONTRIBUTORS',
16
+ // Build / package manifests
17
+ 'package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', 'tsconfig.json',
18
+ 'tsconfig.*.json', 'vitest.config.ts', 'vite.config.ts', '.npmrc', '.nvmrc',
19
+ // VCS / editor
20
+ '.gitignore', '.gitattributes', '.editorconfig', '.gitkeep',
21
+ // Project-local config dirs (peaks-cli convention)
22
+ 'openspec', '.peaks', '.claude', '.peaksrc',
23
+ // Source dirs (writes into them are normal)
24
+ 'src', 'tests', 'bin', 'scripts', 'schemas', 'output-styles', 'docs',
25
+ // Skills are allowed at root
26
+ 'skills',
27
+ // Generated / ignored
28
+ 'dist', 'node_modules', 'coverage', '.nyc_output',
29
+ ]);
30
+ export function isRootWrite(input) {
31
+ const projectRoot = resolve(input.projectRoot);
32
+ const filePath = resolve(input.filePath);
33
+ const rel = filePath.startsWith(projectRoot)
34
+ ? filePath.slice(projectRoot.length).replace(/^[\\/]+/, '')
35
+ : filePath;
36
+ const topSegment = rel.split(/[\\/]/)[0] ?? '';
37
+ // If file is NOT at root (e.g. src/foo/bar.ts), the top segment is "src"
38
+ // which is in the allowlist. This handler only flags FILES AT THE ROOT
39
+ // (top-level), so check whether the file is exactly at root depth.
40
+ const segments = rel.split(/[\\/]/).filter(Boolean);
41
+ const isAtRoot = segments.length === 1;
42
+ if (!isAtRoot) {
43
+ return { isRoot: false, allowed: true, topSegment, denyReason: '' };
44
+ }
45
+ if (ROOT_FILE_ALLOWLIST.has(topSegment)) {
46
+ return { isRoot: true, allowed: true, topSegment, denyReason: '' };
47
+ }
48
+ return {
49
+ isRoot: true,
50
+ allowed: false,
51
+ topSegment,
52
+ denyReason: `no-root-pollution: file "${rel}" is not in the root allowlist. ` +
53
+ `Move it under docs/, tests/, skills/, or another documented directory, ` +
54
+ `or add it to ROOT_FILE_ALLOWLIST in src/services/audit/enforcers/no-root-pollution.ts.`,
55
+ };
56
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * pre-rd-scan enforcer (L2.2 P1) — verifies the project has been scanned
3
+ * before the rd implementation phase begins.
4
+ *
5
+ * Two red lines:
6
+ * - rl-pre-rd-scan-001: peaks scan archetype must have been run
7
+ * - rl-pre-rd-scan-002: peaks standards preflight must have been run
8
+ *
9
+ * Detected by checking for the project-scan.md and standards reports in
10
+ * the session dir. Both are produced by the pre-RD workflow.
11
+ */
12
+ export interface PreRdScanInput {
13
+ readonly projectRoot: string;
14
+ readonly sessionId: string;
15
+ }
16
+ export interface PreRdScanResult {
17
+ readonly archetypeScanned: boolean;
18
+ readonly archetypeReportPath: string;
19
+ readonly standardsPreflightDone: boolean;
20
+ readonly standardsReportPath: string;
21
+ }
22
+ export declare function checkPreRdScan(input: PreRdScanInput): PreRdScanResult;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * pre-rd-scan enforcer (L2.2 P1) — verifies the project has been scanned
3
+ * before the rd implementation phase begins.
4
+ *
5
+ * Two red lines:
6
+ * - rl-pre-rd-scan-001: peaks scan archetype must have been run
7
+ * - rl-pre-rd-scan-002: peaks standards preflight must have been run
8
+ *
9
+ * Detected by checking for the project-scan.md and standards reports in
10
+ * the session dir. Both are produced by the pre-RD workflow.
11
+ */
12
+ import { existsSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ export function checkPreRdScan(input) {
15
+ const archetypeReportPath = join(input.projectRoot, '.peaks/_runtime', input.sessionId, 'rd/project-scan.md');
16
+ const standardsReportPath = join(input.projectRoot, '.peaks/_runtime', input.sessionId, 'standards-preflight.json');
17
+ return {
18
+ archetypeScanned: existsSync(archetypeReportPath),
19
+ archetypeReportPath,
20
+ standardsPreflightDone: existsSync(standardsReportPath),
21
+ standardsReportPath,
22
+ };
23
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * prototype-fidelity enforcer (L2.2 P1) — verifies a prototype is
3
+ * functional (not a stub).
4
+ *
5
+ * Two red lines:
6
+ * - rl-prototype-fidelity-001: prototype files must not contain TODO/FIXME/XXX
7
+ * - rl-prototype-fidelity-002: prototype must have at least 1 passing test
8
+ *
9
+ * (L2.2 ships the source-level check; deeper test-running integration is
10
+ * deferred to a follow-up slice.)
11
+ */
12
+ export interface PrototypeFidelityInput {
13
+ readonly projectRoot: string;
14
+ readonly filePaths: readonly string[];
15
+ }
16
+ export interface PrototypeFidelityResult {
17
+ readonly stubMarkers: readonly {
18
+ filePath: string;
19
+ pattern: string;
20
+ snippet: string;
21
+ }[];
22
+ readonly testFiles: readonly string[];
23
+ }
24
+ export declare function findStubMarkers(input: PrototypeFidelityInput): PrototypeFidelityResult;
25
+ export declare function findTestFiles(projectRoot: string, sourceDir: string): readonly string[];