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,486 @@
1
+ /**
2
+ * red-lines-service — main entry. Orchestrates the three tree scanners,
3
+ * the classifier, and the backing detector, then assembles the final
4
+ * RedLineAudit envelope.
5
+ *
6
+ * Pipeline (per openspec/changes/2026-06-11-l2-1-redlines-audit/design.md):
7
+ * 1. Run all 3 scanners in parallel (skills, rules, openspec)
8
+ * 2. Classifier turns MarkdownLine[] into RedLineEntry[]
9
+ * 3. Backing detector re-classifies each entry (cli-backed vs partial vs prose-only)
10
+ * 4. Tally + return RedLineAudit
11
+ *
12
+ * Sub-agent-sid enforcer (Task 2) is also invoked here: it dogfoods Slice 0.5
13
+ * sid-naming-guard and adds any invalid sids as warnings.
14
+ */
15
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import { classifyFiles } from './classifier.js';
18
+ import { classifyBackingBatch } from './backing-detector.js';
19
+ import { scanSkillsTree } from './scanners/skills-tree-scanner.js';
20
+ import { scanRulesTree } from './scanners/rules-tree-scanner.js';
21
+ import { scanOpenSpecTree } from './scanners/openspec-scanner.js';
22
+ import { findInvalidSubAgentSids, findInvalidRuntimeSids } from './enforcers/sub-agent-sid.js';
23
+ import { checkTechDocPresence } from './enforcers/tech-doc-presence.js';
24
+ import { findStubMarkers } from './enforcers/prototype-fidelity.js';
25
+ import { checkDesignDraftConfirmation } from './enforcers/design-draft-confirm.js';
26
+ import { checkPreRdScan } from './enforcers/pre-rd-scan.js';
27
+ import { readSkillFiles, lintSectionShape, lintSectionOrder, lintFrontmatterShape, lintReferenceLoadStrategy, } from './enforcers/lint-style.js';
28
+ import { lintRefPathResolves, lintNoBrokenMkdir, lintNoPwdSymlinkJumps, lintNoRelativeArchivePaths, } from './enforcers/lint-reference-integrity.js';
29
+ import { lintCliBackMandatorText, lintCliBackNoOrphanBlocking, lintCliBackNoOrphanMustNot, } from './enforcers/lint-cli-back.js';
30
+ import { lintNoFluff, lintNoClosingPrompt, lintStatusHeader, } from './enforcers/lint-output-style.js';
31
+ import { lintOpenSpecAcceptanceBullets, lintOpenSpecSpecReference, lintTechDocPresenceShape, lintPeaksDoctorAcknowledged, } from './enforcers/lint-workflow-shape.js';
32
+ import { lintCatalogSize, lintCatalogProseOnlyRatio, } from './enforcers/lint-catalog-governance.js';
33
+ import { lintH1TitleRequired, lintApplicableTaskLevels, lintSeeAlsoSection, lintCrossRefResolves, lintNoSelfReference, lintNoOrphanLink, lintLineCountLe800, lintH2CountLe12, lintOverviewNearTop, lintLoadStrategyOnDemandFallback, lintLoadStrategyAlwaysCacheable, lintNoBashHeredoc, lintNoSudo, lintNoCurlPipeBash, lintCodeBlockLanguage, lintNoFakePrompt, lintNoAbsolutePaths, lintNoChmod777, lintNoMagicNumbers, lintSkillCitesEveryReference, lintLoadStrategyMatchesSize, readReferenceFiles, } from './enforcers/lint-reference-shape.js';
34
+ import { lintCatalogStability, lintNoOrphanEnforcer, lintNoOrphanCatalog, lintRuntimeBudget, readCatalogHistory, } from './enforcers/lint-audit-regression.js';
35
+ function buildFileInputs(skills, rules, openspec) {
36
+ const grouped = new Map();
37
+ for (const line of [...skills.lines, ...rules.lines, ...openspec.lines]) {
38
+ const existing = grouped.get(line.file);
39
+ if (existing) {
40
+ // line numbers are 1-based; pad to ensure the right slot
41
+ while (existing.length < line.line)
42
+ existing.push('');
43
+ existing[line.line - 1] = line.text;
44
+ }
45
+ else {
46
+ const arr = [];
47
+ while (arr.length < line.line - 1)
48
+ arr.push('');
49
+ arr.push(line.text);
50
+ grouped.set(line.file, arr);
51
+ }
52
+ }
53
+ return Array.from(grouped.entries()).map(([file, lines]) => ({ file, lines }));
54
+ }
55
+ function tally(entries) {
56
+ let cliBacked = 0;
57
+ let partial = 0;
58
+ let proseOnly = 0;
59
+ for (const entry of entries) {
60
+ if (entry.backing === 'cli-backed')
61
+ cliBacked++;
62
+ else if (entry.backing === 'partial')
63
+ partial++;
64
+ else
65
+ proseOnly++;
66
+ }
67
+ return {
68
+ totalRedLines: entries.length,
69
+ cliBacked,
70
+ partial,
71
+ proseOnly,
72
+ };
73
+ }
74
+ export function runRedLinesAudit(input) {
75
+ // Capture the audit start time. The P2-b runtime-budget enforcer
76
+ // uses this to assert that peaks audit red-lines completes in
77
+ // < 2 seconds on a 100-reference project.
78
+ const auditStartMs = Date.now();
79
+ const skills = scanSkillsTree({ projectRoot: input.projectRoot });
80
+ const rules = scanRulesTree({ projectRoot: input.projectRoot });
81
+ const openspec = scanOpenSpecTree({ projectRoot: input.projectRoot });
82
+ const fileInputs = buildFileInputs(skills, rules, openspec);
83
+ const classified = classifyFiles(fileInputs);
84
+ const backed = classifyBackingBatch(classified.entries, input.projectRoot);
85
+ // Sub-agent-sid enforcer (Task 2): dogfoods Slice 0.5 sid-naming-guard.
86
+ const subAgentSids = findInvalidSubAgentSids(input.projectRoot);
87
+ const runtimeSids = findInvalidRuntimeSids(input.projectRoot);
88
+ const warnings = [
89
+ ...skills.warnings,
90
+ ...rules.warnings,
91
+ ...openspec.warnings,
92
+ ...classified.warnings.map((message) => ({ file: '(classifier)', message })),
93
+ ...backed.warnings.map((message) => ({ file: '(backing-detector)', message })),
94
+ ];
95
+ if (subAgentSids.scanned && subAgentSids.invalid.length > 0) {
96
+ for (const sid of subAgentSids.invalid) {
97
+ warnings.push({
98
+ file: '.peaks/_sub_agents/' + sid,
99
+ message: `invalid sub-agent sid: "${sid}" (does not match isValidSessionId)`,
100
+ });
101
+ }
102
+ }
103
+ if (runtimeSids.scanned && runtimeSids.invalid.length > 0) {
104
+ for (const sid of runtimeSids.invalid) {
105
+ warnings.push({
106
+ file: '.peaks/_runtime/' + sid,
107
+ message: `invalid runtime sid: "${sid}" (does not match isValidSessionId)`,
108
+ });
109
+ }
110
+ }
111
+ const counts = tally(backed.entries);
112
+ // L2.4 P2-b: invoke the 5 P0/P1 file-system enforcers during the scan.
113
+ // Each enforcer function returns a structured result; we convert to
114
+ // EnforcerFinding[] and add to the audit output. The audit scanner
115
+ // actually CALLS the enforcers, not just catalogs them.
116
+ const enforcerFindings = [];
117
+ // 1. sub-agent-sid (already partially handled via warnings; here we
118
+ // add a structured finding for the same data).
119
+ if (subAgentSids.scanned && subAgentSids.invalid.length > 0) {
120
+ for (const sid of subAgentSids.invalid) {
121
+ enforcerFindings.push({
122
+ enforcerId: 'rl-sub-agent-sid-001',
123
+ rule: 'Sub-Agent SID Isolation',
124
+ severity: 'fail',
125
+ file: `.peaks/_sub_agents/${sid}`,
126
+ detail: `invalid sub-agent sid: "${sid}" (does not match isValidSessionId)`,
127
+ });
128
+ }
129
+ }
130
+ if (runtimeSids.scanned && runtimeSids.invalid.length > 0) {
131
+ for (const sid of runtimeSids.invalid) {
132
+ enforcerFindings.push({
133
+ enforcerId: 'rl-sub-agent-sid-001',
134
+ rule: 'Sub-Agent SID Isolation',
135
+ severity: 'fail',
136
+ file: `.peaks/_runtime/${sid}`,
137
+ detail: `invalid runtime sid: "${sid}" (does not match isValidSessionId)`,
138
+ });
139
+ }
140
+ }
141
+ // 2. tech-doc-presence — check the current session's tech-doc.md.
142
+ // The sessionId comes from the canonical session binding file
143
+ // (.peaks/_runtime/session.json) when present.
144
+ const sessionJsonPath = `${input.projectRoot}/.peaks/_runtime/session.json`;
145
+ if (existsSync(sessionJsonPath)) {
146
+ try {
147
+ const sessionData = JSON.parse(require('node:fs').readFileSync(sessionJsonPath, 'utf8'));
148
+ if (typeof sessionData.peakSessionId === 'string' && sessionData.peakSessionId.length > 0) {
149
+ const techDoc = checkTechDocPresence({ projectRoot: input.projectRoot, sessionId: sessionData.peakSessionId });
150
+ if (!techDoc.exists) {
151
+ enforcerFindings.push({
152
+ enforcerId: 'rl-tech-doc-presence-001',
153
+ rule: 'Tech-Doc Presence',
154
+ severity: 'fail',
155
+ file: techDoc.path,
156
+ detail: 'tech-doc.md missing (rd → spec-locked transition will refuse)',
157
+ });
158
+ }
159
+ else if (techDoc.isEmpty) {
160
+ enforcerFindings.push({
161
+ enforcerId: 'rl-tech-doc-presence-001',
162
+ rule: 'Tech-Doc Presence',
163
+ severity: 'fail',
164
+ file: techDoc.path,
165
+ detail: 'tech-doc.md is 0 bytes',
166
+ });
167
+ }
168
+ }
169
+ }
170
+ catch {
171
+ // skip malformed session.json
172
+ }
173
+ }
174
+ // 3. pre-rd-scan — check whether project-scan.md and standards-preflight.json exist
175
+ // for the current session.
176
+ if (existsSync(sessionJsonPath)) {
177
+ try {
178
+ const sessionData = JSON.parse(require('node:fs').readFileSync(sessionJsonPath, 'utf8'));
179
+ if (typeof sessionData.peakSessionId === 'string' && sessionData.peakSessionId.length > 0) {
180
+ const preRd = checkPreRdScan({ projectRoot: input.projectRoot, sessionId: sessionData.peakSessionId });
181
+ if (!preRd.archetypeScanned) {
182
+ enforcerFindings.push({
183
+ enforcerId: 'rl-pre-rd-scan-001',
184
+ rule: 'Pre-RD Scan: Archetype Detected',
185
+ severity: 'warn',
186
+ file: preRd.archetypeReportPath,
187
+ detail: 'project-scan.md not produced; rd work has no archetype context',
188
+ });
189
+ }
190
+ if (!preRd.standardsPreflightDone) {
191
+ enforcerFindings.push({
192
+ enforcerId: 'rl-pre-rd-scan-002',
193
+ rule: 'Pre-RD Scan: Standards Preflight',
194
+ severity: 'warn',
195
+ file: preRd.standardsReportPath,
196
+ detail: 'standards-preflight.json not produced; rd work has no project standards context',
197
+ });
198
+ }
199
+ }
200
+ }
201
+ catch {
202
+ // skip
203
+ }
204
+ }
205
+ // 4. design-draft-confirm — check the current change-id's design-draft.md.
206
+ // The change-id is the .peaks/<changeId>/ui/design-draft.md path.
207
+ // For the audit, we look for any .peaks/*/ui/design-draft.md.
208
+ const peaksDir = `${input.projectRoot}/.peaks`;
209
+ if (existsSync(peaksDir)) {
210
+ try {
211
+ const entries = require('node:fs').readdirSync(peaksDir);
212
+ for (const entry of entries) {
213
+ if (entry === '_archive' || entry === '_runtime' || entry === '_sub_agents' || entry.startsWith('.'))
214
+ continue;
215
+ const designCheck = checkDesignDraftConfirmation({
216
+ projectRoot: input.projectRoot,
217
+ sessionId: '',
218
+ changeId: entry,
219
+ });
220
+ if (designCheck.draftExists && !designCheck.confirmed) {
221
+ enforcerFindings.push({
222
+ enforcerId: 'rl-design-draft-confirm-002',
223
+ rule: 'Design-Draft Confirm: Confirmed State',
224
+ severity: 'warn',
225
+ file: designCheck.draftPath,
226
+ detail: 'design-draft.md exists but is not confirmed (no "confirmed" marker)',
227
+ });
228
+ }
229
+ }
230
+ }
231
+ catch {
232
+ // skip
233
+ }
234
+ }
235
+ // 5. prototype-fidelity — scan recent src/ files for stub markers
236
+ // (TODO/FIXME/XXX). Limit to 50 most-recently-modified files
237
+ // to keep scan fast.
238
+ try {
239
+ const srcDir = `${input.projectRoot}/src`;
240
+ if (existsSync(srcDir)) {
241
+ const allFiles = [];
242
+ const walk = (dir) => {
243
+ const ents = require('node:fs').readdirSync(dir, { withFileTypes: true });
244
+ for (const e of ents) {
245
+ const full = `${dir}/${e.name}`;
246
+ if (e.isDirectory()) {
247
+ if (e.name === 'node_modules' || e.name === 'dist')
248
+ continue;
249
+ walk(full);
250
+ }
251
+ else if (e.isFile() && /\.(ts|tsx|js|mjs)$/.test(e.name)) {
252
+ const rel = full.slice(input.projectRoot.length + 1).split('\\').join('/');
253
+ allFiles.push(rel);
254
+ }
255
+ }
256
+ };
257
+ walk(srcDir);
258
+ const sample = allFiles.slice(0, 50);
259
+ const stubHits = findStubMarkers({ projectRoot: input.projectRoot, filePaths: sample });
260
+ for (const hit of stubHits.stubMarkers.slice(0, 10)) {
261
+ enforcerFindings.push({
262
+ enforcerId: 'rl-prototype-fidelity-001',
263
+ rule: 'Prototype Fidelity: No Stub Markers',
264
+ severity: 'warn',
265
+ file: hit.filePath,
266
+ detail: `stub marker "${hit.pattern}" at line containing: ${hit.snippet.slice(0, 50)}`,
267
+ });
268
+ }
269
+ }
270
+ }
271
+ catch {
272
+ // skip
273
+ }
274
+ // 6. P2-a enforcers (Slice #6 L2.3) — 18 lint-style enforcers
275
+ // across Themes A (section), B (frontmatter), C (output
276
+ // style), D (CLI-back gaps), E (reference integrity),
277
+ // F (workflow shape), G (catalog governance). Each enforcer
278
+ // is a pure pattern scan; we walk `skills/` + `references/`,
279
+ // call the helpers, and convert LintHit[] into
280
+ // EnforcerFinding[]. Failures here are WARN, not FAIL —
281
+ // P2-a is the lint layer, not the structural gate layer.
282
+ try {
283
+ const skillsRoot = join(input.projectRoot, 'skills');
284
+ if (existsSync(skillsRoot)) {
285
+ const skillNames = [];
286
+ for (const entry of readdirSync(skillsRoot, { withFileTypes: true })) {
287
+ if (!entry.isDirectory())
288
+ continue;
289
+ if (entry.name.startsWith('.'))
290
+ continue;
291
+ if (!existsSync(join(skillsRoot, entry.name, 'SKILL.md')))
292
+ continue;
293
+ skillNames.push(entry.name);
294
+ }
295
+ const skillFiles = readSkillFiles(skillsRoot, skillNames);
296
+ for (const skill of skillFiles) {
297
+ const refsDir = join(skillsRoot, skill.name, 'references');
298
+ const refs = existsSync(refsDir)
299
+ ? readdirSync(refsDir).filter((f) => f.endsWith('.md'))
300
+ : [];
301
+ const lintHits = [
302
+ ...lintSectionShape(skill),
303
+ ...lintSectionOrder(skill),
304
+ ...lintFrontmatterShape(skill),
305
+ ...lintRefPathResolves(skillsRoot, skill.name, refs),
306
+ ...lintNoBrokenMkdir(skill),
307
+ ...lintNoPwdSymlinkJumps(skill),
308
+ ...lintNoRelativeArchivePaths(skill),
309
+ ...lintReferenceLoadStrategy(refsDir, refs),
310
+ ...lintCliBackMandatorText(skill),
311
+ ...lintCliBackNoOrphanBlocking(skill),
312
+ ...lintCliBackNoOrphanMustNot(skill),
313
+ ...lintNoFluff(skill),
314
+ ...lintNoClosingPrompt(skill),
315
+ ...lintPeaksDoctorAcknowledged(skill),
316
+ ];
317
+ for (const hit of lintHits) {
318
+ enforcerFindings.push({
319
+ enforcerId: hit.catalogId,
320
+ rule: hit.rule,
321
+ severity: 'warn',
322
+ file: hit.file,
323
+ detail: `line ${hit.line}: ${hit.matchedText}`,
324
+ });
325
+ }
326
+ // 7. P2-b enforcers (Slice #7 L2.4) — references/*.md
327
+ // shape enforcers (Themes H-K, M-P). Each enforcer
328
+ // walks the reference files of this skill and reports
329
+ // per-file hits.
330
+ try {
331
+ const refFiles = readReferenceFiles(skillsRoot, skill.name, refs);
332
+ for (const ref of refFiles) {
333
+ const refHits = [
334
+ ...lintH1TitleRequired(ref),
335
+ ...lintApplicableTaskLevels(ref),
336
+ ...lintSeeAlsoSection(ref),
337
+ ...lintCrossRefResolves(ref, refsDir, refs),
338
+ ...lintNoSelfReference(ref),
339
+ ...lintNoOrphanLink(ref),
340
+ ...lintLineCountLe800(ref),
341
+ ...lintH2CountLe12(ref),
342
+ ...lintOverviewNearTop(ref),
343
+ ...lintLoadStrategyOnDemandFallback(ref),
344
+ ...lintLoadStrategyAlwaysCacheable(ref),
345
+ ...lintNoBashHeredoc(ref),
346
+ ...lintNoSudo(ref),
347
+ ...lintNoCurlPipeBash(ref),
348
+ ...lintCodeBlockLanguage(ref),
349
+ ...lintNoFakePrompt(ref),
350
+ ...lintNoAbsolutePaths(ref),
351
+ ...lintNoChmod777(ref),
352
+ ...lintNoMagicNumbers(ref),
353
+ ...lintSkillCitesEveryReference(ref, skill),
354
+ ...lintLoadStrategyMatchesSize(ref),
355
+ ];
356
+ for (const hit of refHits) {
357
+ enforcerFindings.push({
358
+ enforcerId: hit.catalogId,
359
+ rule: hit.rule,
360
+ severity: 'warn',
361
+ file: hit.file,
362
+ detail: `line ${hit.line}: ${hit.matchedText}`,
363
+ });
364
+ }
365
+ }
366
+ }
367
+ catch {
368
+ // skip — P2-b enforcers are best-effort per reference file
369
+ }
370
+ }
371
+ }
372
+ }
373
+ catch {
374
+ // skip — P2-a enforcers are best-effort; a failure here must
375
+ // not break the audit pipeline
376
+ }
377
+ // 8. P2-b Theme L — audit regression enforcers. These check
378
+ // the audit framework's own integrity (catalog stability,
379
+ // no orphan enforcers, no orphan catalog entries, runtime
380
+ // budget). They are the gating layer that `peaks slice check`
381
+ // asserts in its 5th stage.
382
+ try {
383
+ const catalogSize = backed.entries.length;
384
+ const proseOnlyCount = counts.proseOnly;
385
+ const observedMs = Date.now() - auditStartMs;
386
+ const auditRegressionHits = [
387
+ ...lintCatalogStability({
388
+ currentSize: catalogSize,
389
+ sizeNinetyDaysAgo: readCatalogHistory(input.projectRoot),
390
+ }),
391
+ ...lintNoOrphanEnforcer(input.projectRoot),
392
+ ...lintNoOrphanCatalog(),
393
+ ...lintRuntimeBudget(input.projectRoot, observedMs),
394
+ ];
395
+ for (const hit of auditRegressionHits) {
396
+ enforcerFindings.push({
397
+ enforcerId: hit.catalogId,
398
+ rule: hit.rule,
399
+ severity: 'warn',
400
+ file: hit.file,
401
+ detail: `line ${hit.line}: ${hit.matchedText}`,
402
+ });
403
+ }
404
+ }
405
+ catch {
406
+ // skip — audit-regression enforcers are best-effort
407
+ }
408
+ // P2-a Theme F (project-level): openspec proposals + tech-doc shape.
409
+ try {
410
+ const openspecHits = [
411
+ ...lintOpenSpecAcceptanceBullets(input.projectRoot),
412
+ ...lintOpenSpecSpecReference(input.projectRoot),
413
+ ...lintTechDocPresenceShape(input.projectRoot),
414
+ ];
415
+ for (const hit of openspecHits) {
416
+ enforcerFindings.push({
417
+ enforcerId: hit.catalogId,
418
+ rule: hit.rule,
419
+ severity: 'warn',
420
+ file: hit.file,
421
+ detail: `line ${hit.line}: ${hit.matchedText}`,
422
+ });
423
+ }
424
+ }
425
+ catch {
426
+ // skip
427
+ }
428
+ // P2-a Theme C (session log): status header presence. Reads
429
+ // .peaks/_runtime/<sid>/session.log. Soft check: if no session
430
+ // log is present (e.g. dogfood on a fresh project), the enforcer
431
+ // returns an empty array.
432
+ try {
433
+ let peakSessionId = '';
434
+ if (existsSync(sessionJsonPath)) {
435
+ const sessionData = JSON.parse(readFileSync(sessionJsonPath, 'utf8'));
436
+ if (typeof sessionData.peakSessionId === 'string') {
437
+ peakSessionId = sessionData.peakSessionId;
438
+ }
439
+ }
440
+ if (peakSessionId.length > 0) {
441
+ const statusHits = lintStatusHeader(input.projectRoot, peakSessionId);
442
+ for (const hit of statusHits) {
443
+ enforcerFindings.push({
444
+ enforcerId: hit.catalogId,
445
+ rule: hit.rule,
446
+ severity: 'warn',
447
+ file: hit.file,
448
+ detail: `line ${hit.line}: ${hit.matchedText}`,
449
+ });
450
+ }
451
+ }
452
+ }
453
+ catch {
454
+ // skip
455
+ }
456
+ // P2-a Theme G: catalog governance. Two checks: catalog size and
457
+ // prose-only ratio. Both are static checks on the catalog itself,
458
+ // not on the project.
459
+ try {
460
+ const catalogSize = backed.entries.length;
461
+ const proseOnlyCount = counts.proseOnly;
462
+ const catalogSizeHits = lintCatalogSize(catalogSize);
463
+ const ratioHits = lintCatalogProseOnlyRatio(catalogSize, proseOnlyCount);
464
+ for (const hit of [...catalogSizeHits, ...ratioHits]) {
465
+ enforcerFindings.push({
466
+ enforcerId: hit.catalogId,
467
+ rule: hit.rule,
468
+ severity: 'warn',
469
+ file: hit.file,
470
+ detail: `line ${hit.line}: ${hit.matchedText}`,
471
+ });
472
+ }
473
+ }
474
+ catch {
475
+ // skip
476
+ }
477
+ const audit = {
478
+ totalRedLines: counts.totalRedLines,
479
+ cliBacked: counts.cliBacked,
480
+ partial: counts.partial,
481
+ proseOnly: counts.proseOnly,
482
+ audit: backed.entries,
483
+ enforcerFindings,
484
+ };
485
+ return { audit, warnings };
486
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * openspec-scanner — walks every markdown file under `openspec/changes/`
3
+ * (recursive) and returns each file's raw lines. OpenSpec change records
4
+ * often contain red lines for the slice they describe; the audit framework
5
+ * picks them up.
6
+ */
7
+ import type { MarkdownLine, ScanWarning } from '../types.js';
8
+ export interface OpenSpecScanInput {
9
+ readonly projectRoot: string;
10
+ }
11
+ export interface OpenSpecScanResult {
12
+ readonly lines: readonly MarkdownLine[];
13
+ readonly warnings: readonly ScanWarning[];
14
+ }
15
+ export declare function scanOpenSpecTree(input: OpenSpecScanInput): OpenSpecScanResult;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * openspec-scanner — walks every markdown file under `openspec/changes/`
3
+ * (recursive) and returns each file's raw lines. OpenSpec change records
4
+ * often contain red lines for the slice they describe; the audit framework
5
+ * picks them up.
6
+ */
7
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
8
+ import { join, relative } from 'node:path';
9
+ const OPENSPEC_DIR = 'openspec/changes';
10
+ function walkOpenSpecDir(projectRoot, dir, out) {
11
+ let entries;
12
+ try {
13
+ entries = readdirSync(dir, { withFileTypes: true });
14
+ }
15
+ catch (error) {
16
+ out.warnings.push({
17
+ file: relative(projectRoot, dir).split('\\').join('/'),
18
+ message: `readdir failed: ${error instanceof Error ? error.message : String(error)}`,
19
+ });
20
+ return;
21
+ }
22
+ for (const entry of entries) {
23
+ const full = join(dir, entry.name);
24
+ if (entry.isDirectory()) {
25
+ walkOpenSpecDir(projectRoot, full, out);
26
+ }
27
+ else if (entry.isFile() && entry.name.endsWith('.md')) {
28
+ let content;
29
+ try {
30
+ content = readFileSync(full, 'utf-8');
31
+ }
32
+ catch (error) {
33
+ out.warnings.push({
34
+ file: relative(projectRoot, full).split('\\').join('/'),
35
+ message: `read failed: ${error instanceof Error ? error.message : String(error)}`,
36
+ });
37
+ continue;
38
+ }
39
+ const rel = relative(projectRoot, full).split('\\').join('/');
40
+ const lines = content.split(/\r?\n/);
41
+ for (let idx = 0; idx < lines.length; idx++) {
42
+ out.lines.push({ file: rel, line: idx + 1, text: lines[idx] ?? '' });
43
+ }
44
+ }
45
+ }
46
+ }
47
+ export function scanOpenSpecTree(input) {
48
+ const root = join(input.projectRoot, OPENSPEC_DIR);
49
+ if (!existsSync(root)) {
50
+ return { lines: [], warnings: [] };
51
+ }
52
+ const out = { lines: [], warnings: [] };
53
+ walkOpenSpecDir(input.projectRoot, root, out);
54
+ return out;
55
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * rules-tree-scanner — walks every markdown file under `.claude/rules/`
3
+ * (recursive) and returns each file's raw lines for the classifier.
4
+ *
5
+ * Per `static-scan-must-cover-skills-tree-not-just-src.md` and the L2
6
+ * redesign §5.2, the audit must cover .claude/rules in addition to skills.
7
+ */
8
+ import type { MarkdownLine, ScanWarning } from '../types.js';
9
+ export interface RulesTreeScanInput {
10
+ readonly projectRoot: string;
11
+ }
12
+ export interface RulesTreeScanResult {
13
+ readonly lines: readonly MarkdownLine[];
14
+ readonly warnings: readonly ScanWarning[];
15
+ }
16
+ export declare function scanRulesTree(input: RulesTreeScanInput): RulesTreeScanResult;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * rules-tree-scanner — walks every markdown file under `.claude/rules/`
3
+ * (recursive) and returns each file's raw lines for the classifier.
4
+ *
5
+ * Per `static-scan-must-cover-skills-tree-not-just-src.md` and the L2
6
+ * redesign §5.2, the audit must cover .claude/rules in addition to skills.
7
+ */
8
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
9
+ import { join, relative } from 'node:path';
10
+ const RULES_DIR = '.claude/rules';
11
+ function walkRulesDir(projectRoot, dir, out) {
12
+ let entries;
13
+ try {
14
+ entries = readdirSync(dir, { withFileTypes: true });
15
+ }
16
+ catch (error) {
17
+ out.warnings.push({
18
+ file: relative(projectRoot, dir).split('\\').join('/'),
19
+ message: `readdir failed: ${error instanceof Error ? error.message : String(error)}`,
20
+ });
21
+ return;
22
+ }
23
+ for (const entry of entries) {
24
+ const full = join(dir, entry.name);
25
+ if (entry.isDirectory()) {
26
+ walkRulesDir(projectRoot, full, out);
27
+ }
28
+ else if (entry.isFile() && entry.name.endsWith('.md')) {
29
+ let content;
30
+ try {
31
+ content = readFileSync(full, 'utf-8');
32
+ }
33
+ catch (error) {
34
+ out.warnings.push({
35
+ file: relative(projectRoot, full).split('\\').join('/'),
36
+ message: `read failed: ${error instanceof Error ? error.message : String(error)}`,
37
+ });
38
+ continue;
39
+ }
40
+ const rel = relative(projectRoot, full).split('\\').join('/');
41
+ const lines = content.split(/\r?\n/);
42
+ for (let idx = 0; idx < lines.length; idx++) {
43
+ out.lines.push({ file: rel, line: idx + 1, text: lines[idx] ?? '' });
44
+ }
45
+ }
46
+ }
47
+ }
48
+ export function scanRulesTree(input) {
49
+ const rulesRoot = join(input.projectRoot, RULES_DIR);
50
+ if (!existsSync(rulesRoot)) {
51
+ return { lines: [], warnings: [] };
52
+ }
53
+ const out = { lines: [], warnings: [] };
54
+ walkRulesDir(input.projectRoot, rulesRoot, out);
55
+ return out;
56
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * skills-tree-scanner — walks each `skills/<name>/SKILL.md` and returns
3
+ * each file's raw lines for the classifier to consume.
4
+ *
5
+ * Per `static-scan-must-cover-skills-tree-not-just-src.md`, the red-line
6
+ * audit MUST cover the skills tree, not just `src/`. This scanner is the
7
+ * entry point for that coverage.
8
+ */
9
+ import type { MarkdownLine, ScanWarning } from '../types.js';
10
+ export interface SkillsTreeScanInput {
11
+ readonly projectRoot: string;
12
+ }
13
+ export interface SkillsTreeScanResult {
14
+ readonly lines: readonly MarkdownLine[];
15
+ readonly warnings: readonly ScanWarning[];
16
+ }
17
+ export declare function scanSkillsTree(input: SkillsTreeScanInput): SkillsTreeScanResult;