peaks-loop 4.0.15 → 4.0.16

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 (31) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/config/eslint/.peaks-rules.cjs +8 -1
  3. package/dist/cli/commands/_register.js +2 -1
  4. package/dist/cli/commands/code-review-commands.d.ts +2 -30
  5. package/dist/cli/commands/code-review-commands.js +43 -65
  6. package/dist/cli/commands/lint-commands.d.ts +21 -0
  7. package/dist/cli/commands/lint-commands.js +146 -0
  8. package/dist/cli/commands/security-audit-commands.d.ts +1 -1
  9. package/dist/cli/commands/security-audit-commands.js +1 -1
  10. package/dist/services/code-review/ecc-bridge.d.ts +2 -7
  11. package/dist/services/config/config-service.d.ts +2 -19
  12. package/dist/services/config/config-service.js +0 -52
  13. package/dist/services/config/config-types.d.ts +4 -24
  14. package/dist/services/config/config-types.js +1 -10
  15. package/dist/services/lint/detect-eslint.d.ts +10 -0
  16. package/dist/services/lint/detect-eslint.js +59 -0
  17. package/dist/services/lint/detect-ocr-18.d.ts +10 -0
  18. package/dist/services/lint/detect-ocr-18.js +40 -0
  19. package/dist/services/lint/eslint-runner.d.ts +58 -0
  20. package/dist/services/lint/eslint-runner.js +307 -0
  21. package/dist/services/lint/ocr-multilang-adapter.d.ts +34 -0
  22. package/dist/services/lint/ocr-multilang-adapter.js +143 -0
  23. package/package.json +3 -10
  24. package/skills/bee/peaks-rd/SKILL.md +1 -1
  25. package/skills/bee/peaks-rd/references/jsts-eslint-gate.md +197 -0
  26. package/skills/bee/peaks-rd/references/ocr-multilang-1.8.md +132 -0
  27. package/skills/bee/peaks-rd/references/parallel-review-fanout.md +1 -1
  28. package/skills/peaks-code/SKILL.md +1 -1
  29. package/dist/services/code-review/ocr-service.d.ts +0 -129
  30. package/dist/services/code-review/ocr-service.js +0 -372
  31. package/skills/bee/peaks-rd/references/ocr-integration.md +0 -229
package/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.0.16 — 2026-08-06 (ESLint JS/TS Gate + OCR 1.8.x multi-language reviewer + lint strictification)
4
+
5
+ **8 NEW CLI capabilities** (PRD-002 + PRD-002b; 11 commits on main, 26 files, +1509/-960 LOC):
6
+
7
+ - `peaks lint` (parent, un-hidden) — JS/TS ESLint verifier for peaks-rd Gate B5. Calls `npx --package eslint@10.8.0 --package @typescript-eslint/parser@8.66.0 --package @typescript-eslint/eslint-plugin@8.66.0 --package eslint-plugin-import@2.32.0 -- eslint`. Soft-fail (npx missing / errors never block a slice).
8
+ - `peaks lint check` (default) — diffOnly=true + baselineFile waiver + redLineMode='baseline-aware'. Envelope has `state` + `findings` + `summary` + `baselineWaived` + `redLine` + `durationMs`.
9
+ - `peaks lint baseline` — one-shot full-repo scan; writes `.peaks/lint/baseline.json` (project-level, gitignored by default; peak-loop ships its own as a TS CLI reference fixture).
10
+ - `peaks lint --red-line` (or `peaks lint check --red-line`) — generates `.peaks/memory/lint-redline-summary.md` so the next LLM invocation reads prior sibling-violation context before writing new code.
11
+ - `peaks code-review detect-ocr-18` — 5-state probe for OCR 1.8.x install + LLM config readiness.
12
+ - `peaks code-review run-ocr-18 --language <py|go|java|rust|cpp|csharp|ruby|php>` — 8-language multi-language reviewer (AACR-bench coverage).
13
+ - `peaks code-review ocr-18-delegate-preview` — Delegation Mode entry; no LLM key required (host agent's key suffices).
14
+
15
+ **3 NEW runner options** on `src/services/lint/eslint-runner.ts`:
16
+ - `diffOnly` (default `true`): filter ESLint findings to `git diff HEAD` hunks; 存量违规 silently skipped (no-touch-stockcode invariant).
17
+ - `baselineFile` (default `.peaks/lint/baseline.json`): waiver matching on ruleId + file + line; baseline-violations never counted in `summary`.
18
+ - `redLineMode` (default `'baseline-aware'`): aggregate baseline violations by ruleId; output `## Red-line` section for LLM.
19
+
20
+ **Rule severity promotion** (`config/eslint/.peaks-rules.cjs`):
21
+ - `max-lines: ['error', { max: 400, ... }]` (NEW, was implicit `eslint:recommended` only).
22
+ - `max-lines-per-function`: `['warn', 50]` → `['error', 50]`.
23
+ - `complexity: ['warn', 10]` unchanged (soft guidance, not hard limit).
24
+
25
+ **OCR 1.8.9 surface clear-zero** (PRD-002 S2):
26
+ - `@alibaba-group/open-code-review` 1.3.1 peer → 1.8.9 required dep (1.8.x per-platform `optionalDependencies` structurally removes the 2.0.3-era postinstall HTTPS problem).
27
+ - Deleted: `src/services/code-review/ocr-service.ts` (463 lines) + 4 config types + 4 functions + skill ref `ocr-integration.md` (229 lines).
28
+ - 8 languages now supported: Python / Go / Java / Rust / C++ / C# / Ruby / PHP.
29
+
30
+ **7 binding decisions** (PRD-002b):
31
+ - D1 with 4.0.16 release; D2 cost-unbounded single slice; D3 lint for LLM consumption; D4 incremental-first (diffOnly); D5 no-touch-stockcode (baseline waiver); D6 project-aware baseline (downstream MUST regenerate); D7 no downstream-migration slice (`npm update peaks-loop` propagates).
32
+
33
+ **Quality gates**: `pnpm build` exit 0; 19 new BDD tests (8 eslint-runner + 2 lint-commands + 9 ocr-multilang-adapter); cycle-1 stdout narrowing regression preserved at `src/services/lint/eslint-runner.ts:134/144/187`; verify-pipeline 10/10 gates PASS.
34
+
35
+ **5 pre-existing test timeouts** (NOT introduced by this slice; out of scope for 4.0.16; future slice `2026-08-06-fix-pre-existing-test-timeouts`):
36
+ - `tests/unit/services/auto-compact-orchestrator.test.ts:270`
37
+ - `tests/unit/services/skills/presence-lease-service.test.ts:163`
38
+ - `tests/unit/services/session-binding-bridge-path-canonicalize.test.ts:117`
39
+ - `tests/unit/cli/statusline-cli-integration.test.ts:895`
40
+ - (1 more, full-suite-only)
41
+
42
+ **Lockstep bump.** peaks-loop-shared `0.0.45 → 0.0.46` (CLI_VERSION re-stamped to 4.0.16).
43
+
3
44
  ## 4.0.15 — 2026-08-06 (rotation guards tightening + caller-derived workflowId + caller-binding primary source + atomic write hygiene)
4
45
 
5
46
  **Tightens rotation guards with 4th same-process re-resolve short-circuit** (slice `2026-08-06-rotation-guards-and-caller-binding`, commit `f38a796f`):
@@ -70,9 +70,16 @@ module.exports = {
70
70
  ],
71
71
  rules: {
72
72
  // L1 (eslint built-in) — always on, no plugin package required.
73
+ // PRD-002b slice: max-lines + max-lines-per-function promoted warn → error.
74
+ // D5 no-touch-stockcode invariant:存量违规通过 .peaks/lint/baseline.json 豁免
75
+ // (see src/services/lint/eslint-runner.ts baselineFile option).
76
+ 'max-lines': [
77
+ 'error',
78
+ { max: 400, skipBlankLines: true, skipComments: true }
79
+ ],
73
80
  complexity: ['warn', { max: 10 }],
74
81
  'max-lines-per-function': [
75
- 'warn',
82
+ 'error',
76
83
  { max: 50, skipComments: true, skipBlankLines: true }
77
84
  ],
78
85
  'max-params': ['warn', { max: 4 }],
@@ -30,6 +30,7 @@ import { registerIdeCommands } from './ide-commands.js';
30
30
  import { registerImpactCommands } from './impact-commands.js';
31
31
  import { registerJobCommands } from './job-commands.js';
32
32
  import { registerLegacyCommands } from './legacy-commands.js';
33
+ import { registerLintCommands } from './lint-commands.js';
33
34
  import { registerLogCommands } from './log-commands.js';
34
35
  import { registerLoopCommands } from './loop-commands.js';
35
36
  import { registerWorkflowEvalCommands } from './loop-eval-commands.js';
@@ -99,7 +100,7 @@ const REGISTRATIONS = [
99
100
  ['prd-blocks-commands', registerPrdBlocksCommands], ['user-touchpoint-commands', registerUserTouchpointCommands],
100
101
  ['slice-review-commands', registerSliceReviewCommands], ['qa-business-review-commands', registerQaBusinessReviewCommands],
101
102
  ['slice-integrate-commands', registerSliceIntegrateCommands], ['doc-commands', registerDocCommands],
102
- ['legacy-commands', registerLegacyCommands], ['role-commands', registerRoleCommands],
103
+ ['legacy-commands', registerLegacyCommands], ['lint-commands', registerLintCommands], ['role-commands', registerRoleCommands],
103
104
  ['complexity-commands', registerComplexityCommands], ['sub-agent-commands', registerSubAgentCommands],
104
105
  ['sub-agent-shutdown-commands', registerSubAgentShutdownCommands], ['e2e-verify', registerE2EVerifyCommand],
105
106
  ['governance-classify-contract-commands', registerContractCommands],
@@ -1,34 +1,6 @@
1
1
  /**
2
- * peaks code-review * CLI surface — soft-optional ocr integration
3
- * for peaks-rd Gate B3.
4
- *
5
- * Per the "skill-first / CLI-auxiliary" tenet, peaks-rd SKILL.md
6
- * is the primary surface; this CLI returns structured JSON the
7
- * skill consumes to produce a second-opinion code review.
8
- *
9
- * Subcommands:
10
- * - `peaks code-review detect-ocr` — JSON envelope describing the
11
- * current ocr install + config state (5 reasons: ready /
12
- * package-missing / binary-missing / config-missing /
13
- * detection-failed). Source of truth for the LLM endpoint
14
- * config is `peaksConfig.ocr.llm` in the user's peaks-loop
15
- * config (NOT `~/.opencodereview/config.json`).
16
- * - `peaks code-review run-ocr [--from --to --commit]` — invokes
17
- * `ocr review --format json` and wraps the result in a peaks
18
- * ResultEnvelope. Soft-fails when ocr isn't ready so peaks-rd
19
- * can continue without the second-opinion review. Injects the
20
- * LLM endpoint config as env vars (OCR_LLM_URL / OCR_LLM_TOKEN
21
- * / OCR_LLM_MODEL / OCR_USE_ANTHROPIC / OCR_LLM_AUTH_HEADER)
22
- * so the ocr subprocess never has to read from a file the
23
- * user did not set up themselves.
24
- * - `peaks code-review config-template` — prints the JSON snippet
25
- * the user should paste into their peaks-loop config.json. It
26
- * does NOT write anything. The user is in control of their
27
- * LLM token / URL / model. No `peaks ocr config set`; the user
28
- * either edits the JSON directly or uses `peaks config set
29
- * --key ocr.llm.url --value '...'` (a peaks-loop command, not
30
- * an ocr command).
2
+ * peaks code-review * CLI surface — ECC bridge + OCR 1.8.x.
31
3
  */
32
- import { Command } from 'commander';
4
+ import type { Command } from 'commander';
33
5
  import { type ProgramIO } from '../cli-helpers.js';
34
6
  export declare function registerCodeReviewCommands(program: Command, io: ProgramIO): void;
@@ -1,83 +1,61 @@
1
- import { getOcrConfigTemplate, detectOcr, runOcrReview } from '../../services/code-review/ocr-service.js';
2
- import { getOcrLlmConfig, getUserConfigPath, redactConfigSecrets } from '../../services/config/config-service.js';
3
1
  import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
4
2
  import { fail, ok } from 'peaks-loop-shared/result';
3
+ import { detectOcr18 } from '../../services/lint/detect-ocr-18.js';
4
+ import { OCR_18_LANGUAGES, runOcr18 } from '../../services/lint/ocr-multilang-adapter.js';
5
+ const SUPPORTED_LANGUAGES_SET = new Set(OCR_18_LANGUAGES);
5
6
  export function registerCodeReviewCommands(program, io) {
6
7
  const codeReview = program
7
8
  .command('code-review', { hidden: true })
8
- .description('Code-review primitives for peaks-rd Gate B3. Wraps the soft-optional `@alibaba-group/open-code-review` (ocr) tool when it is installed + configured; peaks-rd uses the structured JSON output as a second-opinion review alongside its own LLM review. ocr is a peerDependency of peaks-loop 2.8.2+ (was briefly promoted to a hard dependency in 2.0.1/2.0.2, then demoted to optionalDependencies 2.0.3+ — peer in 2.8.2 because its postinstall downloads a Go binary via HTTPS and would otherwise slow `npm i -g peaks-loop` in restricted environments). Install manually with `npm i -g @alibaba-group/open-code-review` if you want second-opinion reviews. LLM endpoint config lives under `peaksConfig.ocr.llm` in the user config — run `peaks code-review config-template` to see the JSON snippet to paste.');
9
+ .description('Code-review primitives for peaks-rd Gate B3. ECC bridge (JS/TS) + OCR 1.8.x multi-language.');
9
10
  addJsonOption(codeReview
10
- .command('detect-ocr')
11
- .description('Read-only probe: returns the ocr install + config state as a JSON envelope (5 reasons: ready / package-missing / binary-missing / config-missing / detection-failed). peaks-rd calls this first to decide whether to invoke `run-ocr`. Reads the LLM endpoint from `peaksConfig.ocr.llm` (not from ~/.opencodereview/config.json).')
12
- .option('--project <path>', 'project root (default: cwd)')).action((options) => {
13
- const projectRoot = options.project ?? process.cwd();
11
+ .command('detect-ocr-18')
12
+ .description('Read-only probe: returns the 5-state OCR 1.8.x runtime envelope (ready / ocr18-missing / binary-missing / llm-config-missing / detection-failed).')).action((options) => {
14
13
  try {
15
- const detect = detectOcr({
16
- cwd: projectRoot,
17
- peaksConfigPath: getUserConfigPath(),
18
- peaksOcrConfig: getOcrLlmConfig(),
19
- });
20
- const envelope = detect.state === 'ready'
21
- ? ok('code-review.detect-ocr', detect, [...detect.warnings], [...detect.nextActions])
22
- : fail('code-review.detect-ocr', detect.state.toUpperCase().replace(/-/g, '_'), `ocr is not ready: ${detect.state}`, detect, [...detect.nextActions]);
14
+ const result = detectOcr18();
15
+ const envelope = result.state === 'ready'
16
+ ? ok('code-review.detect-ocr-18', result, [...result.warnings], [...result.nextActions])
17
+ : fail('code-review.detect-ocr-18', result.state.toUpperCase().replace(/-/g, '_'), `ocr 1.8.x is not ready: ${result.state}`, result, [...result.nextActions]);
23
18
  printResult(io, envelope, options.json);
24
- if (detect.state !== 'ready') {
25
- process.exitCode = 1;
26
- }
19
+ if (result.state !== 'ready')
20
+ process.exitCode = 0; // soft-fail
27
21
  }
28
22
  catch (error) {
29
- printResult(io, fail('code-review.detect-ocr', 'DETECT_OCR_FAILED', getErrorMessage(error), { state: 'detection-failed', packageInstalled: false, binaryPath: null, version: null, configPath: '', configValid: false, missingKeys: [] }, ['Re-run with `--project <path>` pointing at a known-good project root.']), options.json);
30
- process.exitCode = 1;
23
+ printResult(io, fail('code-review.detect-ocr-18', 'DETECT_FAILED', getErrorMessage(error), { state: 'detection-failed' }, ['Re-run with a valid npx on PATH.']), options.json);
31
24
  }
32
25
  });
33
26
  addJsonOption(codeReview
34
- .command('run-ocr')
35
- .description('Run `ocr review --format json` and return the parsed JSON envelope. Soft-fails (exit 0, ok=false) when ocr is not ready, so peaks-rd can continue without the second-opinion review and surface the install/config nextActions to the user. The peaks-loop `peaksConfig.ocr.llm` block is injected as OCR_LLM_URL / OCR_LLM_TOKEN / ... env vars so the ocr subprocess never has to read ~/.opencodereview/config.json.')
36
- .option('--project <path>', 'project root (default: cwd)')
37
- .option('--from <ref>', 'git ref to diff from (e.g. main, origin/main)')
38
- .option('--to <ref>', 'git ref to diff to (e.g. HEAD, feature-branch)')
39
- .option('--commit <sha>', 'specific commit SHA to review')).action((options) => {
40
- const projectRoot = options.project ?? process.cwd();
41
- try {
42
- const result = runOcrReview({
43
- cwd: projectRoot,
44
- peaksConfigPath: getUserConfigPath(),
45
- peaksOcrConfig: getOcrLlmConfig(),
46
- input: { projectRoot, ...(options.from !== undefined && { from: options.from }), ...(options.to !== undefined && { to: options.to }), ...(options.commit !== undefined && { commit: options.commit }) },
47
- });
48
- // Soft-fail policy: when ocr is not ready or the subprocess
49
- // failed, we still return a JSON envelope (ok=false) but
50
- // do NOT set process.exitCode — the caller is expected to
51
- // pattern-match on the state and proceed without ocr.
52
- const envelope = result.spawned && result.exitCode === 0
53
- ? ok('code-review.run-ocr', result, [...result.warnings], [...result.nextActions])
54
- : fail('code-review.run-ocr', result.state.toUpperCase().replace(/-/g, '_'), result.spawned ? `ocr review exited ${result.exitCode}` : `ocr is not ready: ${result.state}`, result, [...result.nextActions]);
55
- printResult(io, envelope, options.json);
56
- // Intentionally do NOT set process.exitCode here — soft-fail.
57
- }
58
- catch (error) {
59
- printResult(io, fail('code-review.run-ocr', 'RUN_OCR_FAILED', getErrorMessage(error), { spawned: false, state: 'detection-failed', exitCode: null, stdout: '', stderr: '', durationMs: 0, parsed: null }, ['Run `peaks code-review detect-ocr --json` to inspect ocr install state.']), options.json);
60
- // Soft-fail on exception too — peaks-rd should continue.
27
+ .command('run-ocr-18')
28
+ .description('Invoke `ocr review --filter-language <lang>` (or `ocr delegate preview` when --delegate) for one of 8 supported languages.')
29
+ .option('--language <lang>', `target language: ${OCR_18_LANGUAGES.join(' | ')}`)
30
+ .option('--from <ref>', 'git ref to diff from (e.g. main)')
31
+ .option('--to <ref>', 'git ref to diff to (e.g. HEAD)')
32
+ .option('--commit <sha>', 'specific commit SHA to review')
33
+ .option('--delegate', 'use Delegation Mode (no LLM key required)')).action((options) => {
34
+ const language = options.language;
35
+ if (language === undefined || !SUPPORTED_LANGUAGES_SET.has(language)) {
36
+ printResult(io, fail('code-review.run-ocr-18', 'LANGUAGE_UNSUPPORTED', `unsupported language: ${language ?? '<missing>'}`, { state: 'language-unsupported', supported: OCR_18_LANGUAGES }, [`Pick one of: ${OCR_18_LANGUAGES.join(', ')}`]), options.json);
37
+ return;
61
38
  }
39
+ const result = runOcr18({
40
+ cwd: process.cwd(),
41
+ language: language,
42
+ ...(options.from !== undefined ? { from: options.from } : {}),
43
+ ...(options.to !== undefined ? { to: options.to } : {}),
44
+ ...(options.commit !== undefined ? { commit: options.commit } : {}),
45
+ ...(options.delegate === true ? { delegate: true } : {})
46
+ });
47
+ const envelope = result.state === 'ok'
48
+ ? ok('code-review.run-ocr-18', result, [], [])
49
+ : fail('code-review.run-ocr-18', result.state.toUpperCase().replace(/-/g, '_'), `ocr 1.8.x state: ${result.state}`, result, ['Re-run `peaks code-review detect-ocr-18 --json` to refresh.']);
50
+ printResult(io, envelope, options.json);
62
51
  });
63
52
  addJsonOption(codeReview
64
- .command('config-template')
65
- .description('Print the JSON snippet the user should paste into their peaks-loop config (`peaksConfig.ocr.llm`). This command does NOT write anything. The user is the only party that touches the LLM token / URL / model — peaks-loop never auto-configures the endpoint. Token value is shown as the placeholder "<your-api-key>"; replace it before pasting.')).action((options) => {
66
- const template = getOcrConfigTemplate();
67
- const targetPath = getUserConfigPath();
68
- const currentConfig = getOcrLlmConfig();
69
- const currentRedacted = currentConfig === null ? null : redactConfigSecrets(currentConfig, 'ocr.llm');
70
- const payload = {
71
- targetPath,
72
- currentConfig: currentRedacted,
73
- template,
74
- nextActions: [
75
- `Edit ${targetPath} and add the "ocr" block from the template above.`,
76
- 'OR use peaks-loop config set with one key at a time: `peaks config set --key ocr.llm.url --value \'<url>\'` etc.',
77
- 'The authToken field is sensitive and is stored in the user layer (`~/.peaks/config.json`); peaks-loop will not commit it.',
78
- 'Re-run `peaks code-review detect-ocr --json` to verify the new state is `ready`.',
79
- ],
80
- };
81
- printResult(io, ok('code-review.config-template', payload), options.json);
53
+ .command('ocr-18-delegate-preview')
54
+ .description('Print the Delegation Mode spec for the host agent (no LLM key required).')).action((options) => {
55
+ const result = runOcr18({ cwd: process.cwd(), language: 'python', delegate: true });
56
+ const envelope = result.state === 'ok'
57
+ ? ok('code-review.ocr-18-delegate-preview', result, [], [])
58
+ : fail('code-review.ocr-18-delegate-preview', result.state.toUpperCase().replace(/-/g, '_'), `ocr 1.8.x state: ${result.state}`, result, ['Re-run `peaks code-review detect-ocr-18 --json` to refresh.']);
59
+ printResult(io, envelope, options.json);
82
60
  });
83
61
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `peaks code lint` — read-only ESLint verifier (Gate B5).
3
+ *
4
+ * PRD-002b slice: three subcommands expose the runner's new
5
+ * incremental-first / no-touch-stockcode options to the CLI surface.
6
+ *
7
+ * - `peaks lint baseline` — one-shot full-repo scan; writes
8
+ * `.peaks/lint/baseline.json` (project-level, gitignored by
9
+ * default). Each project regenerates its own baseline; the file
10
+ * is project-aware (D6 binding).
11
+ * - `peaks lint check` — default; diffOnly=true + baselineFile
12
+ * waiver + redLineMode='baseline-aware'. This is the LLM-facing
13
+ * Gate B5 entry point.
14
+ * - `peaks lint --red-line` — write
15
+ * `.peaks/memory/lint-redline-summary.md` so the next LLM
16
+ * invocation can read the cross-project red-line before
17
+ * writing more code (supplementary S2 dual-track).
18
+ */
19
+ import type { Command } from 'commander';
20
+ import { type ProgramIO } from '../cli-helpers.js';
21
+ export declare function registerLintCommands(program: Command, io: ProgramIO): void;
@@ -0,0 +1,146 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
4
+ import { fail, ok } from 'peaks-loop-shared/result';
5
+ import { detectEslint } from '../../services/lint/detect-eslint.js';
6
+ import { runEslint } from '../../services/lint/eslint-runner.js';
7
+ function parseTimeoutMs(value) {
8
+ if (value === undefined)
9
+ return undefined;
10
+ const parsed = Number.parseInt(value, 10);
11
+ if (Number.isNaN(parsed) || parsed <= 0) {
12
+ throw new Error(`--timeout-ms must be a positive integer; received "${value}"`);
13
+ }
14
+ return parsed;
15
+ }
16
+ function writeBaselineJson(cwd, baselineFile, result) {
17
+ const fullPath = join(cwd, baselineFile);
18
+ mkdirSync(dirname(fullPath), { recursive: true });
19
+ const violations = result.findings.map((f) => ({
20
+ ruleId: f.ruleId,
21
+ file: f.filePath,
22
+ line: f.line,
23
+ severity: f.severity,
24
+ message: f.message
25
+ }));
26
+ const payload = {
27
+ version: 1,
28
+ generatedAt: new Date().toISOString(),
29
+ toolVersion: 'peaks-loop-4.0.16+',
30
+ violations
31
+ };
32
+ writeFileSync(fullPath, JSON.stringify(payload, null, 2), 'utf8');
33
+ return { path: fullPath, violations: violations.length };
34
+ }
35
+ function writeRedLineSummary(cwd, result) {
36
+ const fullPath = join(cwd, '.peaks/memory/lint-redline-summary.md');
37
+ mkdirSync(dirname(fullPath), { recursive: true });
38
+ const lines = [];
39
+ lines.push('---');
40
+ lines.push('title: lint red-line summary (auto-generated)');
41
+ lines.push('rid: 2026-08-06-eslint-strict-metrics');
42
+ lines.push('generatedAt: ' + new Date().toISOString());
43
+ lines.push('source: `peaks lint --red-line` (PRD-002b slice)');
44
+ lines.push('---');
45
+ lines.push('');
46
+ lines.push('# Lint red-line summary');
47
+ lines.push('');
48
+ lines.push('> LLM-facing card. The next LLM invocation MUST read this file before writing JS/TS code under the peaks-rd Gate B5 surface.');
49
+ lines.push('');
50
+ lines.push('## Top ruleIds (sorted by baseline violation count)');
51
+ lines.push('');
52
+ if (result.redLine.length === 0) {
53
+ lines.push('No baseline violations recorded — project is lint-clean against the current baseline.');
54
+ }
55
+ else {
56
+ for (const entry of result.redLine) {
57
+ lines.push(`- \`${entry.ruleId}\`: ${entry.count} occurrence${entry.count === 1 ? '' : 's'}`);
58
+ for (const top of entry.topFiles) {
59
+ lines.push(` - ${top.file} (${top.count})`);
60
+ }
61
+ }
62
+ }
63
+ lines.push('');
64
+ writeFileSync(fullPath, lines.join('\n'), 'utf8');
65
+ return { path: fullPath, ruleIds: result.redLine.map((r) => r.ruleId) };
66
+ }
67
+ export function registerLintCommands(program, io) {
68
+ const lint = program
69
+ .command('lint')
70
+ .description('Read-only ESLint verifier (peaks-rd Gate B5). Soft-fails when the toolchain is missing.');
71
+ addJsonOption(lint
72
+ .command('detect-eslint', { isDefault: false })
73
+ .description('Read-only probe: returns the 5-state ESLint runtime envelope (ready / eslint-missing / config-error / npx-failed / detection-failed).')).action((options) => {
74
+ const result = detectEslint();
75
+ const envelope = result.state === 'ready'
76
+ ? ok('code.lint.detect-eslint', result, [...result.warnings], [...result.nextActions])
77
+ : fail('code.lint.detect-eslint', result.state.toUpperCase().replace(/-/g, '_'), `eslint is not ready: ${result.state}`, result, [...result.nextActions]);
78
+ printResult(io, envelope, options.json);
79
+ if (result.state !== 'ready') {
80
+ process.exitCode = 0; // soft-fail
81
+ }
82
+ });
83
+ addJsonOption(lint
84
+ .command('check', { isDefault: true })
85
+ .description('Run the read-only ESLint verifier on diff hunks + apply baseline waiver + emit red-line (default Gate B5 entry).')
86
+ .option('--scope <path>', 'lint scope (default: project root)')
87
+ .option('--config <path>', 'explicit ESLint config path')
88
+ .option('--timeout-ms <ms>', 'subprocess timeout in milliseconds (default 60000)')
89
+ .option('--baseline-file <path>', 'baseline JSON path (default .peaks/lint/baseline.json)')
90
+ .option('--red-line', 'also write .peaks/memory/lint-redline-summary.md')).action((options) => {
91
+ let timeoutMs;
92
+ try {
93
+ timeoutMs = parseTimeoutMs(options.timeoutMs);
94
+ }
95
+ catch (error) {
96
+ printResult(io, fail('code.lint', 'INVALID_TIMEOUT', getErrorMessage(error), { state: 'execution-failed' }, ['Re-run with --timeout-ms <positive integer>.']), options.json);
97
+ return;
98
+ }
99
+ const cwd = process.cwd();
100
+ const runOptions = {
101
+ cwd,
102
+ ...(options.scope !== undefined ? { scope: options.scope } : {}),
103
+ ...(options.configPath !== undefined ? { configPath: options.configPath } : {}),
104
+ ...(options.baselineFile !== undefined ? { baselineFile: options.baselineFile } : {}),
105
+ ...(timeoutMs !== undefined ? { timeoutMs } : {})
106
+ };
107
+ const result = runEslint(runOptions);
108
+ const extras = {};
109
+ if (options.redLine === true) {
110
+ extras.redLineSummary = writeRedLineSummary(cwd, result);
111
+ }
112
+ const envelope = result.state === 'ok' || result.state === 'baseline-missing'
113
+ ? ok('code.lint', { ...result, ...extras }, [], [])
114
+ : fail('code.lint', result.state.toUpperCase().replace(/-/g, '_'), `peaks code lint state: ${result.state}`, { ...result, ...extras }, [`Re-run with --scope <path> or restore the ESLint toolchain.`]);
115
+ printResult(io, envelope, options.json);
116
+ });
117
+ addJsonOption(lint
118
+ .command('baseline')
119
+ .description('One-shot full-repo scan; writes .peaks/lint/baseline.json (project-level, gitignored by default).')
120
+ .option('--scope <path>', 'lint scope (default: project root)')
121
+ .option('--config <path>', 'explicit ESLint config path')
122
+ .option('--timeout-ms <ms>', 'subprocess timeout in milliseconds (default 60000)')
123
+ .option('--baseline-file <path>', 'baseline JSON output path (default .peaks/lint/baseline.json)')).action((options) => {
124
+ let timeoutMs;
125
+ try {
126
+ timeoutMs = parseTimeoutMs(options.timeoutMs);
127
+ }
128
+ catch (error) {
129
+ printResult(io, fail('code.lint.baseline', 'INVALID_TIMEOUT', getErrorMessage(error), { state: 'execution-failed' }, ['Re-run with --timeout-ms <positive integer>.']), options.json);
130
+ return;
131
+ }
132
+ const cwd = process.cwd();
133
+ const runOptions = {
134
+ cwd,
135
+ diffOnly: false,
136
+ ...(options.scope !== undefined ? { scope: options.scope } : {}),
137
+ ...(options.configPath !== undefined ? { configPath: options.configPath } : {}),
138
+ ...(timeoutMs !== undefined ? { timeoutMs } : {})
139
+ };
140
+ const result = runEslint(runOptions);
141
+ const baselineFile = options.baselineFile ?? '.peaks/lint/baseline.json';
142
+ const written = writeBaselineJson(cwd, baselineFile, result);
143
+ const envelope = ok('code.lint.baseline', { state: result.state, findings: result.findings.length, ...written }, [], ['Commit baseline.json (or .gitignore it). Peak-loop ships its own baseline as a reference fixture.']);
144
+ printResult(io, envelope, options.json);
145
+ });
146
+ }
@@ -7,7 +7,7 @@
7
7
  * - `peaks security-audit detect` — JSON envelope describing the
8
8
  * 5-state security-audit runtime (ready / handoff-missing /
9
9
  * template-missing / dispatch-failed / envelope-malformed).
10
- * Mirrors `peaks code-review detect-ocr` semantics.
10
+ * Mirrors the ECC bridge detect semantics.
11
11
  * - `peaks security-audit run` — runs the full pipeline:
12
12
  * detect → read handoff + template → write audit artifact to
13
13
  * `.peaks/_runtime/<sid>/audit/security-<rid>.md`. Reads the
@@ -40,7 +40,7 @@ export function registerSecurityAuditCommands(program, io) {
40
40
  .description('Independent security audit skill driver. Reads the immutable peaks-prd handoff (sha256-locked) + the project-level security-template.md, then writes a structured security-<rid>.md artifact. Decoupled from peaks-rd 5-way fan-out per slice v2.12.0 (Group A, Tier 2). See skills/bee/peaks-security-audit/SKILL.md for the full workflow.');
41
41
  addJsonOption(securityAudit
42
42
  .command('detect')
43
- .description('Read-only probe: returns the 5-state security-audit runtime as a JSON envelope (ready / handoff-missing / template-missing / dispatch-failed / envelope-malformed). The peaks-security-audit skill calls this first to decide whether to invoke `run`. Mirrors `peaks code-review detect-ocr` semantics.')
43
+ .description('Read-only probe: returns the 5-state security-audit runtime as a JSON envelope (ready / handoff-missing / template-missing / dispatch-failed / envelope-malformed). The peaks-security-audit skill calls this first to decide whether to invoke `run`. Mirrors the ECC bridge detect semantics.')
44
44
  .option('--rid <rid>', 'request id (e.g. 2026-06-27-v2-12-...)')
45
45
  .option('--sid <sid>', 'session id (e.g. 2026-06-27-session-...)')
46
46
  .option('--project <path>', 'project root (default: cwd)')).action((options) => {
@@ -5,10 +5,7 @@
5
5
  * `rd/code-review.md` markdown format that `peaks request transition`
6
6
  * Gate B3 already reads (`mustContain: ['## Findings', 'CRITICAL']`).
7
7
  *
8
- * Companion to `ocr-service.ts`: ECC is the in-skill agent option
9
- * (uses peaks-loop's own Agent tool surface), ocr is the external
10
- * subprocess option. Both produce a second-opinion code review that
11
- * the parent RD loop aggregates alongside its own inline review.
8
+ * ECC is the in-skill agent option and the production Gate B3 path.
12
9
  *
13
10
  * === Why a bridge (not a direct swap) ===
14
11
  *
@@ -54,9 +51,7 @@ export interface EccEnvelope {
54
51
  readonly gateAction: EccGateAction;
55
52
  }
56
53
  /**
57
- * 5-state detect result — mirrors `ocr-service.ts` `OcrDetectState`
58
- * (ready / package-missing / binary-missing / config-missing /
59
- * detection-failed). ECC's analogue:
54
+ * 5-state ECC detection result:
60
55
  * - `ready` — ECC plugin is installed and reachable
61
56
  * - `plugin-missing` — `everything-claude-code` not in skill list
62
57
  * - `agent-missing` — plugin present but `code-review` agent absent
@@ -1,4 +1,4 @@
1
- import type { ConfigGetOptions, ConfigLayer, ConfigSetOptions, ConfigV2, OcrAuthHeader, OcrConfig, OcrLlmConfig, PeaksConfig, TokenRef, WorkspaceConfig } from './config-types.js';
1
+ import type { ConfigGetOptions, ConfigLayer, ConfigSetOptions, ConfigV2, PeaksConfig, TokenRef, WorkspaceConfig } from './config-types.js';
2
2
  export { resolveProjectRootForConfig, resolveCanonicalProjectRoot } from './config-safety.js';
3
3
  /**
4
4
  * Load the slim 2.0 `~/.peaks/config.json` file. Returns the parsed
@@ -22,23 +22,6 @@ export type RedactedConfigValue = string | number | boolean | null | RedactedCon
22
22
  [key: string]: RedactedConfigValue;
23
23
  };
24
24
  export declare function redactConfigSecrets(value: unknown, path?: string): RedactedConfigValue;
25
- /**
26
- * Read the ocr LLM endpoint config from the user-layer
27
- * `~/.peaks/config.json`. The user populates this themselves by
28
- * pasting the `peaks code-review config-template` output (or by
29
- * running `peaks config set --key ocr.llm.url --value '...'`).
30
- * peaks-loop never auto-writes these values.
31
- */
32
- export declare function getOcrConfig(): OcrConfig;
33
- /**
34
- * Return the resolved `OcrLlmConfig` block (`peaksConfig.ocr.llm`)
35
- * or `null` when the user has not populated the user config. The
36
- * 5-state OCR detector uses this as the source of truth; when the
37
- * returned block is missing required fields it produces a
38
- * `config-missing` state with a templated `nextActions` payload
39
- * the user can paste into their config.
40
- */
41
- export declare function getOcrLlmConfig(): OcrLlmConfig | null;
42
25
  export declare function bootstrapProjectLanguageConfig(projectRoot: string, language: string): void;
43
26
  export declare function readConfig(projectRoot?: string | null): PeaksConfig;
44
27
  export declare function writeConfig(partial: Partial<PeaksConfig>, layer?: ConfigLayer): void;
@@ -53,6 +36,6 @@ export declare function getWorkspaceConfigForPath(path?: string): WorkspaceConfi
53
36
  export declare function ensureWorkspaceConfigForPath(path?: string): WorkspaceConfig | null;
54
37
  export declare function getWorkspaceConfigForCurrentPath(): WorkspaceConfig | null;
55
38
  export declare function ensureWorkspaceConfigForCurrentPath(): WorkspaceConfig | null;
56
- export type { OcrAuthHeader, OcrConfig, OcrLlmConfig, TokenRef, WorkspaceConfig, PeaksConfig, ConfigLayer };
39
+ export type { TokenRef, WorkspaceConfig, PeaksConfig, ConfigLayer };
57
40
  export { getUserConfigPath } from './config-safety.js';
58
41
  export { globalConfigPath } from './config-migration.js';
@@ -412,58 +412,6 @@ export function redactConfigSecrets(value, path = '') {
412
412
  return [key, redactConfigSecrets(entry, nextPath)];
413
413
  }));
414
414
  }
415
- const OCR_AUTH_HEADERS = new Set(['authorization', 'x-api-key', 'bearer']);
416
- function toOcrLlmConfig(value) {
417
- if (!isRecord(value))
418
- return {};
419
- const url = typeof value.url === 'string' && value.url.trim().length > 0 ? value.url.trim() : undefined;
420
- const authToken = typeof value.authToken === 'string' && value.authToken.length > 0 ? value.authToken : undefined;
421
- const model = typeof value.model === 'string' && value.model.trim().length > 0 ? value.model.trim() : undefined;
422
- const useAnthropic = typeof value.useAnthropic === 'boolean' ? value.useAnthropic : undefined;
423
- const rawAuthHeader = typeof value.authHeader === 'string' ? value.authHeader : undefined;
424
- const authHeader = rawAuthHeader !== undefined && OCR_AUTH_HEADERS.has(rawAuthHeader)
425
- ? rawAuthHeader
426
- : undefined;
427
- return {
428
- ...(url !== undefined ? { url } : {}),
429
- ...(authToken !== undefined ? { authToken } : {}),
430
- ...(model !== undefined ? { model } : {}),
431
- ...(useAnthropic !== undefined ? { useAnthropic } : {}),
432
- ...(authHeader !== undefined ? { authHeader } : {})
433
- };
434
- }
435
- function toOcrConfig(value) {
436
- if (!isRecord(value))
437
- return {};
438
- return {
439
- ...(isRecord(value.llm) ? { llm: toOcrLlmConfig(value.llm) } : {})
440
- };
441
- }
442
- /**
443
- * Read the ocr LLM endpoint config from the user-layer
444
- * `~/.peaks/config.json`. The user populates this themselves by
445
- * pasting the `peaks code-review config-template` output (or by
446
- * running `peaks config set --key ocr.llm.url --value '...'`).
447
- * peaks-loop never auto-writes these values.
448
- */
449
- export function getOcrConfig() {
450
- const userConfig = readUserJsonFile() ?? {};
451
- return toOcrConfig(userConfig.ocr);
452
- }
453
- /**
454
- * Return the resolved `OcrLlmConfig` block (`peaksConfig.ocr.llm`)
455
- * or `null` when the user has not populated the user config. The
456
- * 5-state OCR detector uses this as the source of truth; when the
457
- * returned block is missing required fields it produces a
458
- * `config-missing` state with a templated `nextActions` payload
459
- * the user can paste into their config.
460
- */
461
- export function getOcrLlmConfig() {
462
- const ocr = getOcrConfig();
463
- if (!ocr.llm)
464
- return null;
465
- return ocr.llm;
466
- }
467
415
  function inferHumanLanguage(value) {
468
416
  const normalized = value.trim();
469
417
  if (!normalized) {
@@ -47,38 +47,18 @@ export type WorkspaceConfig = {
47
47
  artifactStorage?: ArtifactStorageConfig;
48
48
  installedCapabilityIds: string[];
49
49
  };
50
- export type OcrAuthHeader = 'authorization' | 'x-api-key' | 'bearer';
51
- export type OcrLlmConfig = {
52
- url?: string;
53
- authToken?: string;
54
- model?: string;
55
- useAnthropic?: boolean;
56
- authHeader?: OcrAuthHeader;
57
- };
58
- export type OcrConfig = {
59
- llm?: OcrLlmConfig;
60
- };
61
50
  /**
62
- * 2.0.1 slim `~/.peaks/config.json` schema. The on-disk file holds
63
- * ONLY `version` + `ocr.llm.*` placeholders. All other settings
64
- * (providers, proxy, workspaces, language/model/economy/swarm
65
- * toggles) live in sidecar files under the same `~/.peaks/`
66
- * directory or in per-project `preferences.json`.
67
- *
68
- * The slim shape is enforced on disk by `loadGlobalConfig`; any
69
- * unknown field on read is silently stripped and the file is
70
- * rewritten with the slim shape, so a hand-written or partially-
71
- * migrated file cannot grow stale fields.
51
+ * 2.0.1 slim `~/.peaks/config.json` schema. Runtime settings live in
52
+ * sidecar files under the same `~/.peaks/` directory or in per-project
53
+ * `preferences.json`.
72
54
  *
73
55
  * NOTE: legacy fields below are kept on the type as `@deprecated`
74
56
  * so existing consumers (`config-service.ts`, `workflow-commands.ts`,
75
57
  * etc.) continue to compile during the migration window. They are
76
- * written to / read from sidecar files at runtime; the slim
77
- * `~/.peaks/config.json` only persists `version` + `ocr`.
58
+ * written to / read from sidecar files at runtime.
78
59
  */
79
60
  export type PeaksConfig = {
80
61
  version: string;
81
- ocr?: OcrConfig;
82
62
  /** @deprecated Moved to `~/.peaks/providers.json` (provider-service.ts) */
83
63
  providers?: ModelProviderConfig;
84
64
  /** @deprecated Moved to `~/.peaks/proxy.json` (proxy-service.ts) */
@@ -4,15 +4,6 @@ import { CLI_VERSION } from 'peaks-loop-shared/version';
4
4
  * file only carries `version` + empty `ocr.llm.*` placeholders.
5
5
  */
6
6
  export const DEFAULT_CONFIG = {
7
- version: CLI_VERSION,
8
- ocr: {
9
- llm: {
10
- url: '',
11
- authToken: '',
12
- model: '',
13
- useAnthropic: false,
14
- authHeader: 'authorization'
15
- }
16
- }
7
+ version: CLI_VERSION
17
8
  };
18
9
  export { isConfigV2 } from './config-migration.js';