peaks-cli 1.4.1 → 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 (219) 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 +142 -165
  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 +81 -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/memory-commands.d.ts +13 -0
  21. package/dist/src/cli/commands/memory-commands.js +60 -0
  22. package/dist/src/cli/commands/openspec-commands.js +37 -0
  23. package/dist/src/cli/commands/preferences-commands.d.ts +2 -0
  24. package/dist/src/cli/commands/preferences-commands.js +147 -0
  25. package/dist/src/cli/commands/retrospective-commands.d.ts +9 -0
  26. package/dist/src/cli/commands/retrospective-commands.js +58 -0
  27. package/dist/src/cli/commands/skill-conformance-commands.d.ts +9 -0
  28. package/dist/src/cli/commands/skill-conformance-commands.js +39 -0
  29. package/dist/src/cli/commands/understand-commands.js +34 -0
  30. package/dist/src/cli/commands/upgrade-commands.d.ts +23 -0
  31. package/dist/src/cli/commands/upgrade-commands.js +57 -0
  32. package/dist/src/cli/commands/workflow-commands.js +70 -0
  33. package/dist/src/cli/commands/workspace-commands.js +86 -0
  34. package/dist/src/cli/program.js +46 -22
  35. package/dist/src/services/agent/ecc-agent-service.d.ts +47 -0
  36. package/dist/src/services/agent/ecc-agent-service.js +143 -0
  37. package/dist/src/services/artifacts/request-artifact-service.js +14 -0
  38. package/dist/src/services/audit/backing-detector.d.ts +24 -0
  39. package/dist/src/services/audit/backing-detector.js +59 -0
  40. package/dist/src/services/audit/classifier.d.ts +38 -0
  41. package/dist/src/services/audit/classifier.js +127 -0
  42. package/dist/src/services/audit/enforcers/active-skill-resolver.d.ts +29 -0
  43. package/dist/src/services/audit/enforcers/active-skill-resolver.js +71 -0
  44. package/dist/src/services/audit/enforcers/design-draft-confirm.d.ts +25 -0
  45. package/dist/src/services/audit/enforcers/design-draft-confirm.js +54 -0
  46. package/dist/src/services/audit/enforcers/lint-audit-regression.d.ts +21 -0
  47. package/dist/src/services/audit/enforcers/lint-audit-regression.js +86 -0
  48. package/dist/src/services/audit/enforcers/lint-catalog-governance.d.ts +27 -0
  49. package/dist/src/services/audit/enforcers/lint-catalog-governance.js +38 -0
  50. package/dist/src/services/audit/enforcers/lint-cli-back.d.ts +16 -0
  51. package/dist/src/services/audit/enforcers/lint-cli-back.js +35 -0
  52. package/dist/src/services/audit/enforcers/lint-output-style.d.ts +11 -0
  53. package/dist/src/services/audit/enforcers/lint-output-style.js +94 -0
  54. package/dist/src/services/audit/enforcers/lint-reference-integrity.d.ts +6 -0
  55. package/dist/src/services/audit/enforcers/lint-reference-integrity.js +83 -0
  56. package/dist/src/services/audit/enforcers/lint-reference-shape.d.ts +30 -0
  57. package/dist/src/services/audit/enforcers/lint-reference-shape.js +272 -0
  58. package/dist/src/services/audit/enforcers/lint-style.d.ts +49 -0
  59. package/dist/src/services/audit/enforcers/lint-style.js +173 -0
  60. package/dist/src/services/audit/enforcers/lint-workflow-shape.d.ts +5 -0
  61. package/dist/src/services/audit/enforcers/lint-workflow-shape.js +141 -0
  62. package/dist/src/services/audit/enforcers/login-gate.d.ts +23 -0
  63. package/dist/src/services/audit/enforcers/login-gate.js +40 -0
  64. package/dist/src/services/audit/enforcers/mock-placement.d.ts +25 -0
  65. package/dist/src/services/audit/enforcers/mock-placement.js +48 -0
  66. package/dist/src/services/audit/enforcers/no-root-pollution.d.ts +21 -0
  67. package/dist/src/services/audit/enforcers/no-root-pollution.js +56 -0
  68. package/dist/src/services/audit/enforcers/pre-rd-scan.d.ts +22 -0
  69. package/dist/src/services/audit/enforcers/pre-rd-scan.js +23 -0
  70. package/dist/src/services/audit/enforcers/prototype-fidelity.d.ts +25 -0
  71. package/dist/src/services/audit/enforcers/prototype-fidelity.js +75 -0
  72. package/dist/src/services/audit/enforcers/resume-detection.d.ts +21 -0
  73. package/dist/src/services/audit/enforcers/resume-detection.js +52 -0
  74. package/dist/src/services/audit/enforcers/solo-code-ban.d.ts +23 -0
  75. package/dist/src/services/audit/enforcers/solo-code-ban.js +27 -0
  76. package/dist/src/services/audit/enforcers/sub-agent-sid.d.ts +25 -0
  77. package/dist/src/services/audit/enforcers/sub-agent-sid.js +63 -0
  78. package/dist/src/services/audit/enforcers/tech-doc-presence.d.ts +28 -0
  79. package/dist/src/services/audit/enforcers/tech-doc-presence.js +35 -0
  80. package/dist/src/services/audit/red-line-catalog-p2-a.d.ts +21 -0
  81. package/dist/src/services/audit/red-line-catalog-p2-a.js +233 -0
  82. package/dist/src/services/audit/red-line-catalog-p2-b.d.ts +19 -0
  83. package/dist/src/services/audit/red-line-catalog-p2-b.js +225 -0
  84. package/dist/src/services/audit/red-line-catalog.d.ts +51 -0
  85. package/dist/src/services/audit/red-line-catalog.js +210 -0
  86. package/dist/src/services/audit/red-lines-service.d.ts +23 -0
  87. package/dist/src/services/audit/red-lines-service.js +486 -0
  88. package/dist/src/services/audit/scanners/openspec-scanner.d.ts +15 -0
  89. package/dist/src/services/audit/scanners/openspec-scanner.js +55 -0
  90. package/dist/src/services/audit/scanners/rules-tree-scanner.d.ts +16 -0
  91. package/dist/src/services/audit/scanners/rules-tree-scanner.js +56 -0
  92. package/dist/src/services/audit/scanners/skills-tree-scanner.d.ts +17 -0
  93. package/dist/src/services/audit/scanners/skills-tree-scanner.js +46 -0
  94. package/dist/src/services/audit/static-service.d.ts +57 -0
  95. package/dist/src/services/audit/static-service.js +125 -0
  96. package/dist/src/services/audit/types.d.ts +69 -0
  97. package/dist/src/services/audit/types.js +13 -0
  98. package/dist/src/services/classify/classify-service.d.ts +42 -0
  99. package/dist/src/services/classify/classify-service.js +122 -0
  100. package/dist/src/services/classify/classify-types.d.ts +79 -0
  101. package/dist/src/services/classify/classify-types.js +90 -0
  102. package/dist/src/services/code-review/ocr-service.d.ts +129 -0
  103. package/dist/src/services/code-review/ocr-service.js +362 -0
  104. package/dist/src/services/config/config-migration.d.ts +32 -0
  105. package/dist/src/services/config/config-migration.js +92 -0
  106. package/dist/src/services/config/config-restore.d.ts +10 -0
  107. package/dist/src/services/config/config-restore.js +47 -0
  108. package/dist/src/services/config/config-rollback.d.ts +13 -0
  109. package/dist/src/services/config/config-rollback.js +26 -0
  110. package/dist/src/services/config/config-service.d.ts +35 -2
  111. package/dist/src/services/config/config-service.js +81 -0
  112. package/dist/src/services/config/config-types.d.ts +58 -0
  113. package/dist/src/services/config/config-types.js +6 -0
  114. package/dist/src/services/doctor/doctor-service.js +96 -0
  115. package/dist/src/services/fuzzy-matching/fuzzy-match-service.d.ts +15 -0
  116. package/dist/src/services/fuzzy-matching/fuzzy-match-service.js +56 -0
  117. package/dist/src/services/fuzzy-matching/types.d.ts +20 -0
  118. package/dist/src/services/fuzzy-matching/types.js +1 -0
  119. package/dist/src/services/ide/adapters/hermes-adapter.d.ts +21 -0
  120. package/dist/src/services/ide/adapters/hermes-adapter.js +51 -0
  121. package/dist/src/services/ide/adapters/openclaw-adapter.d.ts +14 -0
  122. package/dist/src/services/ide/adapters/openclaw-adapter.js +42 -0
  123. package/dist/src/services/ide/ide-registry.js +7 -0
  124. package/dist/src/services/ide/ide-types.d.ts +1 -1
  125. package/dist/src/services/memory/memory-search-service.d.ts +61 -0
  126. package/dist/src/services/memory/memory-search-service.js +80 -0
  127. package/dist/src/services/openspec/openspec-propose-from-doctor-service.d.ts +31 -0
  128. package/dist/src/services/openspec/openspec-propose-from-doctor-service.js +95 -0
  129. package/dist/src/services/preferences/preferences-service.d.ts +6 -0
  130. package/dist/src/services/preferences/preferences-service.js +43 -0
  131. package/dist/src/services/preferences/preferences-types.d.ts +90 -0
  132. package/dist/src/services/preferences/preferences-types.js +38 -0
  133. package/dist/src/services/recommendations/capability-seed-items.js +0 -1
  134. package/dist/src/services/recommendations/capability-seed-mappings.js +0 -1
  135. package/dist/src/services/recommendations/capability-seed-sources.js +0 -1
  136. package/dist/src/services/retrospective/retrospective-search-service.d.ts +37 -0
  137. package/dist/src/services/retrospective/retrospective-search-service.js +75 -0
  138. package/dist/src/services/skills/skill-conformance-service.d.ts +40 -0
  139. package/dist/src/services/skills/skill-conformance-service.js +136 -0
  140. package/dist/src/services/skills/skill-runbook-service.js +44 -10
  141. package/dist/src/services/skills/sync-service.d.ts +43 -0
  142. package/dist/src/services/skills/sync-service.js +99 -0
  143. package/dist/src/services/slice/slice-check-service.js +166 -13
  144. package/dist/src/services/slice/slice-check-types.d.ts +1 -1
  145. package/dist/src/services/standards/migrate-claude-rules-service.d.ts +19 -0
  146. package/dist/src/services/standards/migrate-claude-rules-service.js +193 -0
  147. package/dist/src/services/standards/project-context.d.ts +1 -1
  148. package/dist/src/services/standards/project-context.js +0 -4
  149. package/dist/src/services/standards/project-standards-service.js +1 -3
  150. package/dist/src/services/understand/understand-scan-service.js +15 -2
  151. package/dist/src/services/understand/understand-types.d.ts +26 -0
  152. package/dist/src/services/upgrade/1x-detector-service.d.ts +7 -0
  153. package/dist/src/services/upgrade/1x-detector-service.js +94 -0
  154. package/dist/src/services/upgrade/gitignore-migrate-service.d.ts +56 -0
  155. package/dist/src/services/upgrade/gitignore-migrate-service.js +170 -0
  156. package/dist/src/services/upgrade/upgrade-service.d.ts +47 -0
  157. package/dist/src/services/upgrade/upgrade-service.js +381 -0
  158. package/dist/src/services/workspace/migrate-1-4-1-service.js +1 -1
  159. package/dist/src/services/workspace/sid-naming-guard.d.ts +14 -0
  160. package/dist/src/services/workspace/sid-naming-guard.js +31 -0
  161. package/dist/src/services/workspace/workspace-archive-service.d.ts +19 -0
  162. package/dist/src/services/workspace/workspace-archive-service.js +32 -0
  163. package/dist/src/services/workspace/workspace-clean-service.d.ts +41 -0
  164. package/dist/src/services/workspace/workspace-clean-service.js +86 -0
  165. package/dist/src/services/workspace/workspace-state-service.d.ts +7 -0
  166. package/dist/src/services/workspace/workspace-state-service.js +43 -0
  167. package/dist/src/shared/change-id.js +4 -1
  168. package/dist/src/shared/version.d.ts +1 -1
  169. package/dist/src/shared/version.js +1 -1
  170. package/package.json +10 -8
  171. package/schemas/doctor-report.schema.json +1 -1
  172. package/scripts/install-skills.mjs +296 -12
  173. package/skills/peaks-doctor/SKILL.md +59 -0
  174. package/skills/peaks-doctor/references/doctor-check-catalog.md +31 -0
  175. package/skills/peaks-doctor/references/from-doctor-flow.md +64 -0
  176. package/skills/peaks-doctor/test_prompts.json +17 -0
  177. package/skills/peaks-ide/SKILL.md +2 -0
  178. package/skills/peaks-qa/SKILL.md +9 -7
  179. package/skills/peaks-qa/references/artifact-per-request.md +19 -5
  180. package/skills/peaks-qa/references/qa-perf-test-plan.md +6 -6
  181. package/skills/peaks-qa/references/qa-runbook.md +1 -1
  182. package/skills/peaks-rd/SKILL.md +25 -10
  183. package/skills/peaks-rd/references/ocr-integration.md +214 -0
  184. package/skills/peaks-rd/references/rd-fanout-contracts.md +70 -0
  185. package/skills/peaks-rd/references/rd-runbook.md +1 -1
  186. package/skills/peaks-solo/SKILL.md +11 -5
  187. package/skills/peaks-solo/references/completion-handoff.md +3 -1
  188. package/skills/peaks-solo/references/step-0-55-1x-detection.md +82 -0
  189. package/skills/peaks-solo/references/workflow-gates-and-types.md +9 -0
  190. package/dist/src/cli/commands/shadcn-commands.d.ts +0 -3
  191. package/dist/src/cli/commands/shadcn-commands.js +0 -35
  192. package/dist/src/cli/commands/skill-context-stats-command.d.ts +0 -40
  193. package/dist/src/cli/commands/skill-context-stats-command.js +0 -96
  194. package/dist/src/cli/commands/skill-scope-commands.d.ts +0 -51
  195. package/dist/src/cli/commands/skill-scope-commands.js +0 -310
  196. package/dist/src/services/shadcn/shadcn-service.d.ts +0 -27
  197. package/dist/src/services/shadcn/shadcn-service.js +0 -128
  198. package/dist/src/services/skill-scope/adapters/_stub-helper.d.ts +0 -39
  199. package/dist/src/services/skill-scope/adapters/_stub-helper.js +0 -98
  200. package/dist/src/services/skill-scope/adapters/claude-code.d.ts +0 -59
  201. package/dist/src/services/skill-scope/adapters/claude-code.js +0 -304
  202. package/dist/src/services/skill-scope/adapters/codex.d.ts +0 -2
  203. package/dist/src/services/skill-scope/adapters/codex.js +0 -12
  204. package/dist/src/services/skill-scope/adapters/cursor.d.ts +0 -2
  205. package/dist/src/services/skill-scope/adapters/cursor.js +0 -13
  206. package/dist/src/services/skill-scope/adapters/qoder.d.ts +0 -2
  207. package/dist/src/services/skill-scope/adapters/qoder.js +0 -13
  208. package/dist/src/services/skill-scope/adapters/tongyi.d.ts +0 -2
  209. package/dist/src/services/skill-scope/adapters/tongyi.js +0 -13
  210. package/dist/src/services/skill-scope/adapters/trae.d.ts +0 -2
  211. package/dist/src/services/skill-scope/adapters/trae.js +0 -12
  212. package/dist/src/services/skill-scope/detect.d.ts +0 -81
  213. package/dist/src/services/skill-scope/detect.js +0 -513
  214. package/dist/src/services/skill-scope/registry.d.ts +0 -41
  215. package/dist/src/services/skill-scope/registry.js +0 -83
  216. package/dist/src/services/skill-scope/source-of-truth.d.ts +0 -44
  217. package/dist/src/services/skill-scope/source-of-truth.js +0 -118
  218. package/dist/src/services/skill-scope/types.d.ts +0 -195
  219. package/dist/src/services/skill-scope/types.js +0 -97
@@ -0,0 +1,143 @@
1
+ /**
2
+ * ECC 64 agents soft-optional integration — `peaks agent run`.
3
+ *
4
+ * Per spec §7.2 line 818: "64 agents — Soft Optional — 装了 L3
5
+ * 直接调; 不装 L3 退化到 peaks-cli 自有少数核心诊断". The
6
+ * canonical ECC subprocess contract is:
7
+ *
8
+ * npx ecc consult "<topic>" --target claude
9
+ * npx ecc agent run <agent-name> --target <path> --json
10
+ *
11
+ * The peaks-cli `peaks agent run <name> --target <path> --json`
12
+ * CLI shells out to the second form. When ECC is missing OR
13
+ * `agentShieldEnabled: false`, the audit completes with a
14
+ * peaks-cli-only envelope.
15
+ *
16
+ * Mirrors `static-service.ts` (the ECC AgentShield wrapper from
17
+ * L2.3 P2-a): same `subprocessRunner` injection point, same
18
+ * 5-state reason enum, same soft-fail policy.
19
+ */
20
+ import { spawnSync } from 'node:child_process';
21
+ /**
22
+ * The 12 most-used ECC agents per the upstream
23
+ * everything-claude-code catalog. The full 64-agent list is
24
+ * available at runtime via `npx ecc agent list`; this static
25
+ * subset covers the common L3-doctor dispatch paths.
26
+ */
27
+ export const CANONICAL_ECC_AGENTS = [
28
+ { name: 'security-reviewer', description: 'Audit trust boundary + OWASP top-10' },
29
+ { name: 'code-reviewer', description: 'General-purpose code review' },
30
+ { name: 'typescript-reviewer', description: 'TypeScript-specific review' },
31
+ { name: 'python-reviewer', description: 'Python-specific review' },
32
+ { name: 'golang-reviewer', description: 'Go-specific review' },
33
+ { name: 'rust-reviewer', description: 'Rust-specific review' },
34
+ { name: 'java-reviewer', description: 'Java-specific review' },
35
+ { name: 'cpp-reviewer', description: 'C/C++-specific review' },
36
+ { name: 'backend-patterns', description: 'Backend service patterns' },
37
+ { name: 'frontend-patterns', description: 'Frontend patterns' },
38
+ { name: 'database-migrations', description: 'DB migration safety' },
39
+ { name: 'deployment-patterns', description: 'Deployment / CI-CD patterns' },
40
+ ];
41
+ const ECC_DETECT_TIMEOUT_MS = 5000;
42
+ const ECC_RUN_TIMEOUT_MS = 60000;
43
+ const defaultSubprocessRunner = {
44
+ run(command, args, timeoutMs) {
45
+ try {
46
+ const r = spawnSync(command, args, {
47
+ timeout: timeoutMs,
48
+ encoding: 'utf8',
49
+ stdio: ['ignore', 'pipe', 'pipe'],
50
+ maxBuffer: 16 * 1024 * 1024,
51
+ });
52
+ return {
53
+ status: r.status,
54
+ stdout: r.stdout ?? '',
55
+ stderr: r.stderr ?? '',
56
+ };
57
+ }
58
+ catch (err) {
59
+ return {
60
+ status: null,
61
+ stdout: '',
62
+ stderr: '',
63
+ error: err,
64
+ };
65
+ }
66
+ },
67
+ };
68
+ function isEccInstalled(runner) {
69
+ const result = runner.run('npx', ['ecc', '--version'], ECC_DETECT_TIMEOUT_MS);
70
+ if (result.error)
71
+ return false;
72
+ return result.status === 0;
73
+ }
74
+ /**
75
+ * Validate the agent name against the canonical ECC catalog.
76
+ * Returns `null` if the agent is known; an error message otherwise.
77
+ */
78
+ export function validateEccAgent(agent) {
79
+ if (typeof agent !== 'string' || agent.length === 0) {
80
+ return 'peaks agent run: agent name must be a non-empty string';
81
+ }
82
+ if (!/^[a-z][a-z0-9-]*$/.test(agent)) {
83
+ return `peaks agent run: agent name "${agent}" is invalid (must match [a-z][a-z0-9-]*)`;
84
+ }
85
+ return null;
86
+ }
87
+ export function runEccAgent(input, runner = defaultSubprocessRunner) {
88
+ const enableAgent = input.enableAgent === true;
89
+ const warnings = [];
90
+ let result = null;
91
+ // Resolve the effective "should spawn" decision.
92
+ // flagEnabled (CLI override) > preference (always-on for now; L3+ future) > false
93
+ const shouldSpawn = enableAgent;
94
+ const installed = isEccInstalled(runner);
95
+ let reason;
96
+ let spawned;
97
+ if (!shouldSpawn) {
98
+ reason = 'flag-disabled';
99
+ spawned = false;
100
+ }
101
+ else if (!installed) {
102
+ reason = 'flag-enabled-but-ecc-missing';
103
+ spawned = false;
104
+ warnings.push('`npx ecc --version` failed. Run `npx ecc --help` to install ECC, ' +
105
+ 'then re-run `peaks agent run`. The peaks-cli native diagnostic ' +
106
+ 'still runs via `peaks doctor scan`.');
107
+ }
108
+ else {
109
+ reason = 'enabled-and-installed';
110
+ spawned = true;
111
+ const start = Date.now();
112
+ const subResult = runner.run('npx', ['ecc', 'agent', 'run', input.agent, '--target', input.projectRoot, '--json'], ECC_RUN_TIMEOUT_MS);
113
+ let parsed;
114
+ let error;
115
+ if (subResult.error) {
116
+ error = subResult.error.message;
117
+ }
118
+ else if (subResult.status !== 0) {
119
+ error = `ecc agent run exited with status ${subResult.status}: ${subResult.stderr}`;
120
+ }
121
+ else {
122
+ try {
123
+ parsed = JSON.parse(subResult.stdout);
124
+ }
125
+ catch {
126
+ // Non-JSON output is allowed; peaks-cli surfaces the raw
127
+ // stdout for the human reader and treats the run as
128
+ // ok=true (the subprocess exited 0).
129
+ parsed = undefined;
130
+ }
131
+ }
132
+ result = {
133
+ agent: input.agent,
134
+ ok: error === undefined,
135
+ stdout: subResult.stdout,
136
+ stderr: subResult.stderr,
137
+ durationMs: Date.now() - start,
138
+ ...(parsed !== undefined ? { parsed } : {}),
139
+ ...(error !== undefined ? { error } : {}),
140
+ };
141
+ }
142
+ return { agent: input.agent, spawned, reason, result, warnings };
143
+ }
@@ -783,6 +783,20 @@ export async function transitionRequestArtifact(options) {
783
783
  if (!prerequisiteResult.ok && options.allowIncomplete !== true) {
784
784
  throw new PrerequisitesNotSatisfiedError(options.role, options.newState, existing.sessionId, prerequisiteResult.missing);
785
785
  }
786
+ // L2.1 P0 red line #4: tech-doc-presence. The rd → spec-locked transition
787
+ // is refused if `rd/tech-doc.md` is missing or empty. This is a machine-
788
+ // enforced gate that backs the "MANDATORY tech-doc before spec-locked"
789
+ // prose in the redesign spec §5.4.
790
+ if (options.role === 'rd' && options.newState === 'spec-locked' && options.allowIncomplete !== true) {
791
+ const { checkTechDocPresence, TECH_DOC_MISSING_CODE, TECH_DOC_MISSING_MESSAGE } = await import('../audit/enforcers/tech-doc-presence.js');
792
+ const techDoc = checkTechDocPresence({
793
+ projectRoot: options.projectRoot,
794
+ sessionId: existing.sessionId,
795
+ });
796
+ if (!techDoc.exists || techDoc.isEmpty) {
797
+ throw new PrerequisitesNotSatisfiedError(options.role, options.newState, existing.sessionId, [{ path: techDoc.path, description: `${TECH_DOC_MISSING_CODE}: ${TECH_DOC_MISSING_MESSAGE}` }]);
798
+ }
799
+ }
786
800
  // Type sanity check for PRD handoff
787
801
  if (options.typeSanityCheck !== undefined && options.role === 'prd' && options.newState === 'handed-off') {
788
802
  const sanityReport = checkTypeSanity({
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Backing detector — classifies each red line as `cli-backed`, `partial`,
3
+ * or `prose-only`. The classifier already sets the backing for catalog hits
4
+ * (cli-backed when an enforcer file path is present). This module exists to
5
+ * handle the post-classification nuance: heuristics for the "partial" tier
6
+ * (a gate exists but the LLM can bypass it) and verification that the
7
+ * enforcer file actually exists on disk.
8
+ */
9
+ import type { RedLineEntry } from './types.js';
10
+ export interface BackingResult {
11
+ readonly entry: RedLineEntry;
12
+ readonly enforcerExists: boolean;
13
+ }
14
+ /**
15
+ * Re-classify a single RedLineEntry. Returns a new entry with the
16
+ * `backing` field updated and `enforcerRef` possibly nulled if the
17
+ * referenced file does not exist on disk.
18
+ */
19
+ export declare function classifyBacking(entry: RedLineEntry, projectRoot: string): BackingResult;
20
+ export interface BackingBatchResult {
21
+ readonly entries: readonly RedLineEntry[];
22
+ readonly warnings: readonly string[];
23
+ }
24
+ export declare function classifyBackingBatch(entries: readonly RedLineEntry[], projectRoot: string): BackingBatchResult;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Backing detector — classifies each red line as `cli-backed`, `partial`,
3
+ * or `prose-only`. The classifier already sets the backing for catalog hits
4
+ * (cli-backed when an enforcer file path is present). This module exists to
5
+ * handle the post-classification nuance: heuristics for the "partial" tier
6
+ * (a gate exists but the LLM can bypass it) and verification that the
7
+ * enforcer file actually exists on disk.
8
+ */
9
+ import { existsSync } from 'node:fs';
10
+ import { resolve } from 'node:path';
11
+ const PARTIAL_PHRASES = [
12
+ 'if llm cooperates',
13
+ 'llm-cooperation',
14
+ 'partial cli backing',
15
+ 'best effort',
16
+ 'advisory only',
17
+ 'soft enforcement',
18
+ 'when remembered',
19
+ ];
20
+ function detectPartial(context) {
21
+ const lower = context.toLowerCase();
22
+ return PARTIAL_PHRASES.some((phrase) => lower.includes(phrase));
23
+ }
24
+ /**
25
+ * Re-classify a single RedLineEntry. Returns a new entry with the
26
+ * `backing` field updated and `enforcerRef` possibly nulled if the
27
+ * referenced file does not exist on disk.
28
+ */
29
+ export function classifyBacking(entry, projectRoot) {
30
+ if (detectPartial(entry.source.context)) {
31
+ return {
32
+ entry: { ...entry, backing: 'partial' },
33
+ enforcerExists: entry.enforcerRef !== null && existsSync(resolve(projectRoot, entry.enforcerRef)),
34
+ };
35
+ }
36
+ if (entry.enforcerRef === null) {
37
+ return { entry, enforcerExists: false };
38
+ }
39
+ const enforcerPath = resolve(projectRoot, entry.enforcerRef);
40
+ const exists = existsSync(enforcerPath);
41
+ return {
42
+ entry: { ...entry, backing: exists ? 'cli-backed' : 'prose-only' },
43
+ enforcerExists: exists,
44
+ };
45
+ }
46
+ export function classifyBackingBatch(entries, projectRoot) {
47
+ const updated = [];
48
+ const warnings = [];
49
+ for (const entry of entries) {
50
+ const { entry: reclassified, enforcerExists } = classifyBacking(entry, projectRoot);
51
+ updated.push(reclassified);
52
+ if (reclassified.backing === 'cli-backed' && !enforcerExists) {
53
+ // Defensive: should not happen because classifyBacking downgrades to
54
+ // prose-only, but keep the assertion in case of future drift.
55
+ warnings.push(`enforcer ref "${reclassified.enforcerRef}" missing on disk for ${reclassified.id}`);
56
+ }
57
+ }
58
+ return { entries: updated, warnings };
59
+ }
@@ -0,0 +1,38 @@
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 type { RedLineEntry, RedLineMarker } from './types.js';
13
+ export declare function detectMarker(lineText: string): RedLineMarker | null;
14
+ /**
15
+ * Derive a human-readable rule name from the marker line. Heuristic: take
16
+ * the first 8 words of the line, lowercase, trimmed. Catalog matching is
17
+ * the source of truth for the canonical rule name; this is the fallback
18
+ * when no catalog entry matches.
19
+ */
20
+ export declare function deriveRuleName(lineText: string): string;
21
+ export interface ClassifyFileInput {
22
+ readonly file: string;
23
+ readonly lines: readonly string[];
24
+ }
25
+ export interface ClassifyResult {
26
+ readonly entries: readonly RedLineEntry[];
27
+ readonly warnings: readonly string[];
28
+ }
29
+ /**
30
+ * Classify a single markdown file. Returns 0+ RedLineEntry; one entry per
31
+ * marker hit. Marker hits that appear multiple times on the same line are
32
+ * counted once.
33
+ */
34
+ export declare function classifyFile(input: ClassifyFileInput): ClassifyResult;
35
+ /**
36
+ * Batch wrapper — classify each input file and flatten the entries.
37
+ */
38
+ export declare function classifyFiles(inputs: readonly ClassifyFileInput[]): ClassifyResult;
@@ -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;