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.
- package/CHANGELOG.md +82 -0
- package/README-en.md +12 -9
- package/README.md +15 -4
- package/config/eslint/.peaks-rules.cjs +47 -27
- package/dist/cli/commands/_register.js +4 -2
- package/dist/cli/commands/audit-commands.d.ts +1 -1
- package/dist/cli/commands/code-gate-command.d.ts +25 -0
- package/dist/cli/commands/code-gate-command.js +86 -0
- package/dist/cli/commands/code-mode-gate-commands.js +3 -4
- package/dist/cli/commands/code-orchestrator-can-do.js +2 -1
- package/dist/cli/commands/code-review-commands.d.ts +2 -30
- package/dist/cli/commands/code-review-commands.js +43 -65
- package/dist/cli/commands/cron-commands.js +16 -4
- package/dist/cli/commands/dashboard-long-run.js +8 -4
- package/dist/cli/commands/dispatch-from-dag.d.ts +1 -1
- package/dist/cli/commands/hooks-commands.js +56 -3
- package/dist/cli/commands/lint-commands.d.ts +21 -0
- package/dist/cli/commands/lint-commands.js +146 -0
- package/dist/cli/commands/qa-commands.js +1 -2
- package/dist/cli/commands/security-audit-commands.d.ts +1 -1
- package/dist/cli/commands/security-audit-commands.js +1 -1
- package/dist/cli/commands/sediment-commands.d.ts +1 -1
- package/dist/cli/commands/slice-commands.js +20 -8
- package/dist/cli/commands/statusline-commands.d.ts +1 -0
- package/dist/cli/commands/statusline-commands.js +17 -3
- package/dist/cli/commands/tech-commands.d.ts +1 -1
- package/dist/services/artifacts/request-artifact-service.d.ts +2 -2
- package/dist/services/artifacts/request-artifact-service.js +10 -1
- package/dist/services/audit/enforcers/lint-reference-shape.js +18 -4
- package/dist/services/code/auto-compact-orchestrator.js +9 -1
- package/dist/services/code/batch-heartbeat-poller.js +26 -0
- package/dist/services/code/orchestrator-can-do.d.ts +24 -0
- package/dist/services/code/orchestrator-can-do.js +48 -2
- package/dist/services/code/post-compact-detector.d.ts +1 -1
- package/dist/services/code-review/ecc-bridge.d.ts +2 -7
- package/dist/services/compact-statusline/compact-statusline-service.js +19 -11
- package/dist/services/config/config-service.d.ts +2 -19
- package/dist/services/config/config-service.js +2 -56
- package/dist/services/config/config-types.d.ts +4 -24
- package/dist/services/config/config-types.js +1 -10
- package/dist/services/container/container-lease.js +21 -8
- package/dist/services/context/spillover-store.d.ts +1 -1
- package/dist/services/crystallization/crystallization-types.js +20 -8
- package/dist/services/crystallization/evidence-brief-builder.js +22 -11
- package/dist/services/dispatch/dispatch-record-writer.js +231 -141
- package/dist/services/evolution/evolution-types.js +38 -21
- package/dist/services/feedback/feedback-promotion-service.js +9 -1
- package/dist/services/hooks/pre-tool-code-gate.d.ts +46 -0
- package/dist/services/hooks/pre-tool-code-gate.js +91 -0
- package/dist/services/ide/current-model-detector.js +1 -2
- package/dist/services/lint/detect-eslint.d.ts +10 -0
- package/dist/services/lint/detect-eslint.js +62 -0
- package/dist/services/lint/detect-ocr-18.d.ts +10 -0
- package/dist/services/lint/detect-ocr-18.js +40 -0
- package/dist/services/lint/eslint-runner.d.ts +57 -0
- package/dist/services/lint/eslint-runner.js +370 -0
- package/dist/services/lint/npx-resolver.d.ts +6 -0
- package/dist/services/lint/npx-resolver.js +47 -0
- package/dist/services/lint/ocr-multilang-adapter.d.ts +34 -0
- package/dist/services/lint/ocr-multilang-adapter.js +143 -0
- package/dist/services/loop/loop-release-types.js +32 -13
- package/dist/services/loop/spec-service.d.ts +7 -1
- package/dist/services/loop/spec-service.js +218 -182
- package/dist/services/rd/rd-service.js +22 -9
- package/dist/services/scan/archetype-service.js +21 -9
- package/dist/services/session/binding-store.js +1 -2
- package/dist/services/share/bundle-reader.d.ts +6 -9
- package/dist/services/share/bundle-reader.js +297 -223
- package/dist/services/skills/hooks-settings-service.d.ts +33 -0
- package/dist/services/skills/hooks-settings-service.js +27 -1
- package/dist/services/slice/calibration-store.js +21 -8
- package/dist/services/slice/slice-check-service.js +35 -14
- package/dist/services/slice/slice-decompose-service.js +459 -223
- package/dist/services/standards/project-context.js +135 -73
- package/dist/services/verdict/envelopes.d.ts +1 -2
- package/dist/services/verdict/envelopes.js +1 -2
- package/dist/services/verdict/verdict-aggregator.d.ts +1 -2
- package/dist/services/vm/vm-lease.js +19 -8
- package/dist/services/worktree/worktree-lease.d.ts +0 -10
- package/dist/services/worktree/worktree-lease.js +19 -8
- package/dist/shared/fs-utils.d.ts +1 -1
- package/package.json +8 -12
- package/skills/bee/peaks-rd/SKILL.md +1 -1
- package/skills/bee/peaks-rd/references/jsts-eslint-gate.md +197 -0
- package/skills/bee/peaks-rd/references/ocr-multilang-1.8.md +132 -0
- package/skills/bee/peaks-rd/references/parallel-review-fanout.md +1 -1
- package/skills/peaks-code/SKILL.md +47 -1
- package/dist/services/code-review/ocr-service.d.ts +0 -129
- package/dist/services/code-review/ocr-service.js +0 -372
- 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.
|
|
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
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 (
|
|
25
|
-
process.exitCode =
|
|
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', '
|
|
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('
|
|
36
|
-
.option('--
|
|
37
|
-
.option('--from <ref>', 'git ref to diff from (e.g. main
|
|
38
|
-
.option('--to <ref>', 'git ref to diff to (e.g. HEAD
|
|
39
|
-
.option('--commit <sha>', 'specific commit SHA to review')
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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('
|
|
65
|
-
.description('Print the
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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,
|
|
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
|
|
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' ?
|
|
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' ?
|
|
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 /
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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) => {
|
|
@@ -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),
|
|
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(
|
|
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(
|
|
101
|
-
lines.push('-'.repeat(
|
|
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(
|
|
104
|
-
r.mtime.padEnd(
|
|
105
|
-
String(r.sizeBytes).padEnd(
|
|
106
|
-
(r.pickedPath ? 'yes' : 'no').padEnd(
|
|
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
|