peaks-loop 4.0.15 → 4.0.17

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 (90) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README-en.md +12 -9
  3. package/README.md +15 -4
  4. package/config/eslint/.peaks-rules.cjs +47 -27
  5. package/dist/cli/commands/_register.js +4 -2
  6. package/dist/cli/commands/audit-commands.d.ts +1 -1
  7. package/dist/cli/commands/code-gate-command.d.ts +25 -0
  8. package/dist/cli/commands/code-gate-command.js +86 -0
  9. package/dist/cli/commands/code-mode-gate-commands.js +3 -4
  10. package/dist/cli/commands/code-orchestrator-can-do.js +2 -1
  11. package/dist/cli/commands/code-review-commands.d.ts +2 -30
  12. package/dist/cli/commands/code-review-commands.js +43 -65
  13. package/dist/cli/commands/cron-commands.js +16 -4
  14. package/dist/cli/commands/dashboard-long-run.js +8 -4
  15. package/dist/cli/commands/dispatch-from-dag.d.ts +1 -1
  16. package/dist/cli/commands/hooks-commands.js +56 -3
  17. package/dist/cli/commands/lint-commands.d.ts +21 -0
  18. package/dist/cli/commands/lint-commands.js +146 -0
  19. package/dist/cli/commands/qa-commands.js +1 -2
  20. package/dist/cli/commands/security-audit-commands.d.ts +1 -1
  21. package/dist/cli/commands/security-audit-commands.js +1 -1
  22. package/dist/cli/commands/sediment-commands.d.ts +1 -1
  23. package/dist/cli/commands/slice-commands.js +20 -8
  24. package/dist/cli/commands/statusline-commands.d.ts +1 -0
  25. package/dist/cli/commands/statusline-commands.js +17 -3
  26. package/dist/cli/commands/tech-commands.d.ts +1 -1
  27. package/dist/services/artifacts/request-artifact-service.d.ts +2 -2
  28. package/dist/services/artifacts/request-artifact-service.js +10 -1
  29. package/dist/services/audit/enforcers/lint-reference-shape.js +18 -4
  30. package/dist/services/code/auto-compact-orchestrator.js +9 -1
  31. package/dist/services/code/batch-heartbeat-poller.js +26 -0
  32. package/dist/services/code/orchestrator-can-do.d.ts +24 -0
  33. package/dist/services/code/orchestrator-can-do.js +48 -2
  34. package/dist/services/code/post-compact-detector.d.ts +1 -1
  35. package/dist/services/code-review/ecc-bridge.d.ts +2 -7
  36. package/dist/services/compact-statusline/compact-statusline-service.js +19 -11
  37. package/dist/services/config/config-service.d.ts +2 -19
  38. package/dist/services/config/config-service.js +2 -56
  39. package/dist/services/config/config-types.d.ts +4 -24
  40. package/dist/services/config/config-types.js +1 -10
  41. package/dist/services/container/container-lease.js +21 -8
  42. package/dist/services/context/spillover-store.d.ts +1 -1
  43. package/dist/services/crystallization/crystallization-types.js +20 -8
  44. package/dist/services/crystallization/evidence-brief-builder.js +22 -11
  45. package/dist/services/dispatch/dispatch-record-writer.js +231 -141
  46. package/dist/services/evolution/evolution-types.js +38 -21
  47. package/dist/services/feedback/feedback-promotion-service.js +9 -1
  48. package/dist/services/hooks/pre-tool-code-gate.d.ts +46 -0
  49. package/dist/services/hooks/pre-tool-code-gate.js +91 -0
  50. package/dist/services/ide/current-model-detector.js +1 -2
  51. package/dist/services/lint/detect-eslint.d.ts +10 -0
  52. package/dist/services/lint/detect-eslint.js +62 -0
  53. package/dist/services/lint/detect-ocr-18.d.ts +10 -0
  54. package/dist/services/lint/detect-ocr-18.js +40 -0
  55. package/dist/services/lint/eslint-runner.d.ts +57 -0
  56. package/dist/services/lint/eslint-runner.js +370 -0
  57. package/dist/services/lint/npx-resolver.d.ts +6 -0
  58. package/dist/services/lint/npx-resolver.js +47 -0
  59. package/dist/services/lint/ocr-multilang-adapter.d.ts +34 -0
  60. package/dist/services/lint/ocr-multilang-adapter.js +143 -0
  61. package/dist/services/loop/loop-release-types.js +32 -13
  62. package/dist/services/loop/spec-service.d.ts +7 -1
  63. package/dist/services/loop/spec-service.js +218 -182
  64. package/dist/services/rd/rd-service.js +22 -9
  65. package/dist/services/scan/archetype-service.js +21 -9
  66. package/dist/services/session/binding-store.js +1 -2
  67. package/dist/services/share/bundle-reader.d.ts +6 -9
  68. package/dist/services/share/bundle-reader.js +297 -223
  69. package/dist/services/skills/hooks-settings-service.d.ts +33 -0
  70. package/dist/services/skills/hooks-settings-service.js +27 -1
  71. package/dist/services/slice/calibration-store.js +21 -8
  72. package/dist/services/slice/slice-check-service.js +35 -14
  73. package/dist/services/slice/slice-decompose-service.js +459 -223
  74. package/dist/services/standards/project-context.js +135 -73
  75. package/dist/services/verdict/envelopes.d.ts +1 -2
  76. package/dist/services/verdict/envelopes.js +1 -2
  77. package/dist/services/verdict/verdict-aggregator.d.ts +1 -2
  78. package/dist/services/vm/vm-lease.js +19 -8
  79. package/dist/services/worktree/worktree-lease.d.ts +0 -10
  80. package/dist/services/worktree/worktree-lease.js +19 -8
  81. package/dist/shared/fs-utils.d.ts +1 -1
  82. package/package.json +8 -12
  83. package/skills/bee/peaks-rd/SKILL.md +1 -1
  84. package/skills/bee/peaks-rd/references/jsts-eslint-gate.md +197 -0
  85. package/skills/bee/peaks-rd/references/ocr-multilang-1.8.md +132 -0
  86. package/skills/bee/peaks-rd/references/parallel-review-fanout.md +1 -1
  87. package/skills/peaks-code/SKILL.md +47 -1
  88. package/dist/services/code-review/ocr-service.d.ts +0 -129
  89. package/dist/services/code-review/ocr-service.js +0 -372
  90. package/skills/bee/peaks-rd/references/ocr-integration.md +0 -229
@@ -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
  }
@@ -34,7 +34,19 @@ import { findProjectRoot } from '../../services/config/config-safety.js';
34
34
  const SCHEDULE_VERSION = 1;
35
35
  const SCHEDULE_FILENAME = 'schedule.json';
36
36
  const HISTORY_FILENAME = 'history.jsonl';
37
- const DEFAULT_INTERVAL_MS = 24 * 60 * 60 * 1_000; // 24h
37
+ /**
38
+ * PRD-002b slice 2 — extract cron-orchestration magic numbers (24h
39
+ * default interval, 5-minute exec timeout, stderr-message truncation
40
+ * lengths in the renderer).
41
+ */
42
+ const MS_PER_SECOND = 1_000;
43
+ const MINUTES_PER_HOUR = 60;
44
+ const SECONDS_PER_MINUTE = 60;
45
+ const HOURS_PER_DAY = 24;
46
+ const DEFAULT_INTERVAL_MS = HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND;
47
+ const EXEC_TIMEOUT_MS = 5 * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND;
48
+ const STDERR_RECORD_TRUNCATE_BYTES = 2000;
49
+ const STDERR_NEXT_ACTIONS_TRUNCATE_CHARS = 200;
38
50
  function cronDir(projectRoot) {
39
51
  return join(projectRoot, '.peaks', 'cron');
40
52
  }
@@ -136,13 +148,13 @@ export function runTask(projectRoot, task) {
136
148
  cwd: projectRoot,
137
149
  stdio: ['ignore', 'pipe', 'pipe'],
138
150
  encoding: 'utf8',
139
- timeout: 5 * 60_000
151
+ timeout: EXEC_TIMEOUT_MS
140
152
  });
141
153
  }
142
154
  catch (err) {
143
155
  const e = err;
144
156
  exitCode = typeof e.status === 'number' ? e.status : 1;
145
- stderr = (e.stderr ?? getErrorMessage(err)).slice(0, 2000);
157
+ stderr = (e.stderr ?? getErrorMessage(err)).slice(0, STDERR_RECORD_TRUNCATE_BYTES);
146
158
  }
147
159
  const record = {
148
160
  id,
@@ -243,7 +255,7 @@ export function registerCronCommand(program, io) {
243
255
  updatedEntries.push({ ...task, lastRunAt: record.finishedAt });
244
256
  }
245
257
  writeSchedule(projectRoot, { version: SCHEDULE_VERSION, entries: updatedEntries });
246
- printResult(io, ok('cron.run', { projectRoot, ran: records.length, records }, records.filter((r) => r.exitCode !== 0).map((r) => `Task ${r.taskId} failed (exit=${r.exitCode}): ${r.stderr.slice(0, 200)}`), [`Ran ${records.length} task(s); ${records.filter((r) => r.exitCode === 0).length} succeeded.`]), options.json);
258
+ printResult(io, ok('cron.run', { projectRoot, ran: records.length, records }, records.filter((r) => r.exitCode !== 0).map((r) => `Task ${r.taskId} failed (exit=${r.exitCode}): ${r.stderr.slice(0, STDERR_NEXT_ACTIONS_TRUNCATE_CHARS)}`), [`Ran ${records.length} task(s); ${records.filter((r) => r.exitCode === 0).length} succeeded.`]), options.json);
247
259
  if (records.some((r) => r.exitCode !== 0))
248
260
  process.exitCode = 1;
249
261
  }
@@ -5,7 +5,11 @@ import { read24hState } from '../../services/24h-mode/index.js';
5
5
  import { getErrorMessage } from '../cli-helpers.js';
6
6
  const SINCE_PATTERN = /^(\d+)([smhd])$/i;
7
7
  const WINDOW_CAP_HOURS = 24;
8
- const WINDOW_CAP_MS = WINDOW_CAP_HOURS * 60 * 60 * 1000;
8
+ const MS_PER_SECOND = 1000;
9
+ const MS_PER_MINUTE = 60 * MS_PER_SECOND;
10
+ const MS_PER_HOUR = 60 * MS_PER_MINUTE;
11
+ const MS_PER_DAY = 24 * MS_PER_HOUR;
12
+ const WINDOW_CAP_MS = WINDOW_CAP_HOURS * MS_PER_HOUR;
9
13
  function parseSince(raw) {
10
14
  if (!raw)
11
15
  return { ok: false, error: '--since is required (e.g. 24h, 8h, 30m)' };
@@ -16,7 +20,7 @@ function parseSince(raw) {
16
20
  if (!Number.isFinite(value) || value < 0)
17
21
  return { ok: false, error: `--since must be a non-negative integer (got ${raw})` };
18
22
  const unit = (match[2] ?? 'h').toLowerCase();
19
- const multiplier = unit === 'd' ? 86_400_000 : unit === 'h' ? 3_600_000 : unit === 'm' ? 60_000 : 1_000;
23
+ const multiplier = unit === 'd' ? MS_PER_DAY : unit === 'h' ? MS_PER_HOUR : unit === 'm' ? MS_PER_MINUTE : MS_PER_SECOND;
20
24
  return { ok: true, ms: Math.min(WINDOW_CAP_MS, value * multiplier) };
21
25
  }
22
26
  function readSlices(projectRoot, sessionId) {
@@ -45,7 +49,7 @@ function boundaryLabel(raw) {
45
49
  return { key: 'valid', message: 'unparseable' };
46
50
  const value = Number(match[1]);
47
51
  const unit = (match[2] ?? 'h').toLowerCase();
48
- const multiplier = unit === 'd' ? 86_400_000 : unit === 'h' ? 3_600_000 : unit === 'm' ? 60_000 : 1_000;
52
+ const multiplier = unit === 'd' ? MS_PER_DAY : unit === 'h' ? MS_PER_HOUR : unit === 'm' ? MS_PER_MINUTE : MS_PER_SECOND;
49
53
  if (value * multiplier > WINDOW_CAP_MS) {
50
54
  return { key: 'cap', message: 'since>24h is capped to the 24h window' };
51
55
  }
@@ -86,7 +90,7 @@ export function registerDashboardLongRunCommand(dashboard, io) {
86
90
  const sliceCount = readSlices(projectRoot, sid);
87
91
  const checkpointAgeMs = readLastCheckpointAgeMs(snapshot);
88
92
  const checkpointFrequency = snapshot.checkpoints > 0 && snapshot.enteredAt
89
- ? Math.max(1, Math.round((Date.now() - Date.parse(snapshot.enteredAt)) / snapshot.checkpoints / 60_000))
93
+ ? Math.max(1, Math.round((Date.now() - Date.parse(snapshot.enteredAt)) / snapshot.checkpoints / MS_PER_MINUTE))
90
94
  : null;
91
95
  const indicators = {
92
96
  dispatchCount: sliceCount,
@@ -1,3 +1,3 @@
1
- import type { ProgramIO } from '../cli-helpers.js';
1
+ import { type ProgramIO } from '../cli-helpers.js';
2
2
  import { type DispatchOptions } from './sub-agent-shared.js';
3
3
  export declare function runDispatchFromDag(role: string, options: DispatchOptions, asJson: boolean, io: ProgramIO): Promise<void>;
@@ -43,7 +43,16 @@ function listExpectedEntriesForIde(ide, _skipProgress = false) {
43
43
  if (ide === 'trae') {
44
44
  return [{ matcher: adapter.toolMatcher, sentinel: 'peaks hook handle' }];
45
45
  }
46
- return [{ matcher: adapter.toolMatcher, sentinel: 'peaks gate enforce' }];
46
+ // Slice 2026-08-06-codegate-vendor-neutral: Claude Code install
47
+ // also emits the Edit|Write|MultiEdit code-gate entry. The summary
48
+ // mirrors the install shape, NOT a hardcoded expected list. The
49
+ // hook source of truth is `src/services/hooks/pre-tool-code-gate.sh`
50
+ // (vendor-neutral); the runtime adapter is `peaks code-gate --json`
51
+ // registered as a PreToolUse entry.
52
+ return [
53
+ { matcher: adapter.toolMatcher, sentinel: 'peaks gate enforce' },
54
+ { matcher: 'Edit|Write|MultiEdit', sentinel: 'peaks code-gate' }
55
+ ];
47
56
  }
48
57
  /**
49
58
  * Slice 2026-07-24-peaks-code-bridge-002-rootcause (G6b / G10): copy the
@@ -98,6 +107,26 @@ function copyBridgeHookIfPresent(userHome) {
98
107
  copyFileSync(source, target);
99
108
  return { copied: true, source, target };
100
109
  }
110
+ /**
111
+ * Slice 2026-08-06-codegate-vendor-neutral: copy the code-gate hook
112
+ * source (`src/services/hooks/pre-tool-code-gate.sh`) into the user-
113
+ * global peaks-code hooks dir, mirroring the bridge-hook copy. The
114
+ * hook itself is vendor-neutral (no `claude` / `anthropic` strings in
115
+ * the script) — this installer is the vendor adapter that decides
116
+ * where the file lands. The CLI command `peaks code-gate` is the
117
+ * runtime entry; the shell script is the canonical artifact distributed
118
+ * alongside the bridge hook.
119
+ */
120
+ function copyCodeGateHookIfPresent(userHome) {
121
+ const source = resolve(__dirname, '..', '..', 'services', 'hooks', 'pre-tool-code-gate.sh');
122
+ const target = resolve(userHome, '.claude', 'skills', 'peaks-code', 'hooks', 'pre-tool-code-gate.sh');
123
+ if (!existsSync(source)) {
124
+ return { copied: false, source, target };
125
+ }
126
+ mkdirSync(dirname(target), { recursive: true });
127
+ copyFileSync(source, target);
128
+ return { copied: true, source, target };
129
+ }
101
130
  export function registerHooksCommands(program, io) {
102
131
  const hooks = program
103
132
  .command('hooks')
@@ -121,6 +150,14 @@ export function registerHooksCommands(program, io) {
121
150
  const bridgeCopy = scope === 'global'
122
151
  ? copyBridgeHookIfPresent(process.env.USERPROFILE ?? process.env.HOME ?? '')
123
152
  : { copied: false, source: '', target: '' };
153
+ // Slice 2026-08-06-codegate-vendor-neutral: also copy the
154
+ // code-gate hook script. The runtime gate lives at
155
+ // `peaks code-gate --json` (registered as a PreToolUse entry
156
+ // in the install output); the script is the build artifact
157
+ // distributed alongside the bridge hook.
158
+ const codeGateCopy = scope === 'global'
159
+ ? copyCodeGateHookIfPresent(process.env.USERPROFILE ?? process.env.HOME ?? '')
160
+ : { copied: false, source: '', target: '' };
124
161
  printResult(io, ok('hooks.install', {
125
162
  ...plan,
126
163
  ide,
@@ -129,6 +166,7 @@ export function registerHooksCommands(program, io) {
129
166
  skipProgress,
130
167
  entries: dryRunEntries,
131
168
  bridgeHookCopy: bridgeCopy,
169
+ codeGateHookCopy: codeGateCopy,
132
170
  // Slice 2026-07-29-worktree-layer3-deny: surface the
133
171
  // Layer 3 deny entries the install WOULD write. The
134
172
  // dry-run branch never actually invokes
@@ -141,7 +179,10 @@ export function registerHooksCommands(program, io) {
141
179
  `would write ${listSuperpowersDenyEntries().length} permissions.deny entries (Layer 3 worktree governance)`,
142
180
  bridgeCopy.copied
143
181
  ? `would copy bridge hook from ${bridgeCopy.source} to ${bridgeCopy.target}`
144
- : 'would not copy bridge hook (source missing or non-global scope)'
182
+ : 'would not copy bridge hook (source missing or non-global scope)',
183
+ codeGateCopy.copied
184
+ ? `would copy code-gate hook from ${codeGateCopy.source} to ${codeGateCopy.target}`
185
+ : 'would not copy code-gate hook (source missing or non-global scope)'
145
186
  ]), options.json);
146
187
  return;
147
188
  }
@@ -161,6 +202,14 @@ export function registerHooksCommands(program, io) {
161
202
  const bridgeCopy = scope === 'global'
162
203
  ? copyBridgeHookIfPresent(process.env.USERPROFILE ?? process.env.HOME ?? '')
163
204
  : { copied: false, source: '', target: '' };
205
+ // Slice 2026-08-06-codegate-vendor-neutral: also copy the
206
+ // code-gate hook script. The runtime gate lives at
207
+ // `peaks code-gate --json` (registered as a PreToolUse entry
208
+ // in the install output); the script is the build artifact
209
+ // distributed alongside the bridge hook.
210
+ const codeGateCopy = scope === 'global'
211
+ ? copyCodeGateHookIfPresent(process.env.USERPROFILE ?? process.env.HOME ?? '')
212
+ : { copied: false, source: '', target: '' };
164
213
  const nextActions = result.applied
165
214
  ? [
166
215
  'Restart the IDE (or reload the workspace) so the hook entries take effect',
@@ -170,7 +219,10 @@ export function registerHooksCommands(program, io) {
170
219
  `Layer 3 deny: wrote ${listSuperpowersDenyEntries().length} permissions.deny entries (worktree governance)`,
171
220
  bridgeCopy.copied
172
221
  ? `Copied bridge hook: ${bridgeCopy.target}`
173
- : 'Bridge hook not copied (source missing or non-global scope)'
222
+ : 'Bridge hook not copied (source missing or non-global scope)',
223
+ codeGateCopy.copied
224
+ ? `Copied code-gate hook: ${codeGateCopy.target}`
225
+ : 'Code-gate hook not copied (source missing or non-global scope)'
174
226
  ]
175
227
  : (bridgeCopy.copied
176
228
  ? [`Bridge hook copied: ${bridgeCopy.target}`]
@@ -189,6 +241,7 @@ export function registerHooksCommands(program, io) {
189
241
  skipProgress,
190
242
  entries: installedEntries.map((e) => ({ matcher: e.matcher, sentinel: e.sentinel })),
191
243
  bridgeHookCopy: bridgeCopy,
244
+ codeGateHookCopy: codeGateCopy,
192
245
  permissionsDenyApplied: result.applied,
193
246
  permissionsDenyEntries: denyEntries
194
247
  }, [], nextActions), options.json);
@@ -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
+ }
@@ -25,8 +25,7 @@
25
25
  * with reason `--no-browser`. This is a slice-level opt-out for
26
26
  * backend-only or already-covered-by-unit-tests work.
27
27
  */
28
- import { join } from 'node:path';
29
- import { resolve } from 'node:path';
28
+ import { join, resolve } from 'node:path';
30
29
  import { addJsonOption, printResult } from '../cli-helpers.js';
31
30
  import { fail, ok } from 'peaks-loop-shared/result';
32
31
  import { archiveScreenshots } from '../../services/qa/screenshot-archive-service.js';
@@ -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) => {
@@ -1,5 +1,5 @@
1
1
  import type { Command } from "commander";
2
- import type { ProgramIO } from "../cli-helpers.js";
2
+ import { type ProgramIO } from "../cli-helpers.js";
3
3
  export interface CliResult {
4
4
  ok: boolean;
5
5
  error?: string;
@@ -9,6 +9,17 @@ import { decompose as multiPassDecompose } from '../../services/slice/multi-pass
9
9
  import { readResult as readDecompositionResult, writeResult as writeSchemaResult } from '../../services/slice/schema-router.js';
10
10
  import { fail, ok } from 'peaks-loop-shared/result';
11
11
  import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
12
+ /**
13
+ * PRD-002b slice 2 — extract CLI listing-pagination + table-format
14
+ * constants. Values are bytewise-identical to the original literals.
15
+ */
16
+ const SLICE_LIST_DEFAULT_LIMIT = 50;
17
+ const SLICE_LIST_MAX_LIMIT = 500;
18
+ const SLICE_LIST_RID_COL_WIDTH = 34;
19
+ const SLICE_LIST_MTIME_COL_WIDTH = 22;
20
+ const SLICE_LIST_SIZE_COL_WIDTH = 8;
21
+ const SLICE_LIST_PICKED_COL_WIDTH = 6;
22
+ const SLICE_LIST_SEPARATOR_WIDTH = 76;
12
23
  export function registerSliceCommands(program, io) {
13
24
  const slice = program.command('slice').description('Slice lifecycle: check (boundary), decompose (PRD -> 6-stage algorithm), ' +
14
25
  'pick (fzf multi-select), plan (apply via peaks request init). ' +
@@ -67,12 +78,12 @@ export function registerSliceCommands(program, io) {
67
78
  '(mtime > 30d). Use --stale-only to filter, --rid <substring> to narrow, ' +
68
79
  '--limit <n> to cap. To remove stale entries, use `peaks slice cleanup` (separate slice).')
69
80
  .option('--project <path>', 'target project root', '.')
70
- .option('--limit <n>', 'cap result count (default 50, max 500)', (v) => parseInt(v, 10), 50)
81
+ .option('--limit <n>', 'cap result count (default 50, max 500)', (v) => parseInt(v, 10), SLICE_LIST_DEFAULT_LIMIT)
71
82
  .option('--stale-only', 'only include rids older than the stale threshold', false)
72
83
  .option('--rid <substring>', 'case-insensitive substring filter on rid', '')).action((options) => {
73
84
  try {
74
85
  const projectRoot = resolveCanonicalProjectRoot(options.project);
75
- const limit = Math.max(1, Math.min(500, options.limit ?? 50));
86
+ const limit = Math.max(1, Math.min(SLICE_LIST_MAX_LIMIT, options.limit ?? SLICE_LIST_DEFAULT_LIMIT));
76
87
  const all = listDecompositions(projectRoot);
77
88
  let filtered = all;
78
89
  const ridSub = (options.rid ?? '').toLowerCase();
@@ -97,13 +108,13 @@ export function registerSliceCommands(program, io) {
97
108
  }
98
109
  // Plaintext mode: header + table
99
110
  const lines = [];
100
- lines.push('RID'.padEnd(34) + 'MTIME'.padEnd(22) + 'SIZE'.padEnd(8) + 'PICKED'.padEnd(6) + 'STALE');
101
- lines.push('-'.repeat(76));
111
+ lines.push('RID'.padEnd(SLICE_LIST_RID_COL_WIDTH) + 'MTIME'.padEnd(SLICE_LIST_MTIME_COL_WIDTH) + 'SIZE'.padEnd(SLICE_LIST_SIZE_COL_WIDTH) + 'PICKED'.padEnd(SLICE_LIST_PICKED_COL_WIDTH) + 'STALE');
112
+ lines.push('-'.repeat(SLICE_LIST_SEPARATOR_WIDTH));
102
113
  for (const r of page) {
103
- lines.push(r.rid.padEnd(34) +
104
- r.mtime.padEnd(22) +
105
- String(r.sizeBytes).padEnd(8) +
106
- (r.pickedPath ? 'yes' : 'no').padEnd(6) +
114
+ lines.push(r.rid.padEnd(SLICE_LIST_RID_COL_WIDTH) +
115
+ r.mtime.padEnd(SLICE_LIST_MTIME_COL_WIDTH) +
116
+ String(r.sizeBytes).padEnd(SLICE_LIST_SIZE_COL_WIDTH) +
117
+ (r.pickedPath ? 'yes' : 'no').padEnd(SLICE_LIST_PICKED_COL_WIDTH) +
107
118
  (r.isStale ? 'yes' : 'no'));
108
119
  }
109
120
  if (truncated) {
@@ -365,6 +376,7 @@ function writeDecompositionFile(rid, result, projectRoot) {
365
376
  * recorded in .peaks/memory/ for follow-up). If that slice ships with a
366
377
  * different default, both must update here.
367
378
  */
379
+ // eslint-disable-next-line no-magic-numbers -- canonical 30-day-in-ms math
368
380
  const STALE_THRESHOLD_MS = 30 * 24 * 60 * 60 * 1000;
369
381
  /**
370
382
  * Enumerate every slice decomposition artifact under
@@ -3,6 +3,7 @@ import { type ProgramIO } from '../cli-helpers.js';
3
3
  type RenderOptions = {
4
4
  project?: string;
5
5
  json?: boolean;
6
+ now?: number | string;
6
7
  };
7
8
  /**
8
9
  * Default-statusline render body. Reused by both the top-level default