peaks-cli 1.4.2 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +51 -0
- package/CHANGELOG.md +279 -0
- package/README-en.md +226 -0
- package/README.md +152 -122
- package/dist/src/cli/commands/agent-commands.d.ts +20 -0
- package/dist/src/cli/commands/agent-commands.js +48 -0
- package/dist/src/cli/commands/audit-commands.d.ts +18 -0
- package/dist/src/cli/commands/audit-commands.js +138 -0
- package/dist/src/cli/commands/capability-commands.js +2 -1
- package/dist/src/cli/commands/classify-classify-commands.d.ts +19 -0
- package/dist/src/cli/commands/classify-classify-commands.js +151 -0
- package/dist/src/cli/commands/code-review-commands.d.ts +34 -0
- package/dist/src/cli/commands/code-review-commands.js +83 -0
- package/dist/src/cli/commands/config-commands.js +90 -0
- package/dist/src/cli/commands/context-commands.d.ts +21 -0
- package/dist/src/cli/commands/context-commands.js +167 -0
- package/dist/src/cli/commands/core-artifact-commands.js +60 -2
- package/dist/src/cli/commands/hook-handle.js +50 -0
- package/dist/src/cli/commands/loop-commands.d.ts +21 -0
- package/dist/src/cli/commands/loop-commands.js +128 -0
- package/dist/src/cli/commands/openspec-commands.js +37 -0
- package/dist/src/cli/commands/preferences-commands.d.ts +2 -0
- package/dist/src/cli/commands/preferences-commands.js +147 -0
- package/dist/src/cli/commands/skill-conformance-commands.d.ts +9 -0
- package/dist/src/cli/commands/skill-conformance-commands.js +39 -0
- package/dist/src/cli/commands/understand-commands.js +34 -0
- package/dist/src/cli/commands/upgrade-commands.d.ts +23 -0
- package/dist/src/cli/commands/upgrade-commands.js +57 -0
- package/dist/src/cli/commands/workflow-commands.js +70 -0
- package/dist/src/cli/commands/workspace-commands.js +117 -2
- package/dist/src/cli/program.js +30 -0
- package/dist/src/lib/render/message-renderer.d.ts +20 -0
- package/dist/src/lib/render/message-renderer.js +80 -0
- package/dist/src/services/agent/ecc-agent-service.d.ts +47 -0
- package/dist/src/services/agent/ecc-agent-service.js +143 -0
- package/dist/src/services/artifacts/request-artifact-service.js +14 -0
- package/dist/src/services/audit/backing-detector.d.ts +24 -0
- package/dist/src/services/audit/backing-detector.js +59 -0
- package/dist/src/services/audit/classifier.d.ts +38 -0
- package/dist/src/services/audit/classifier.js +127 -0
- package/dist/src/services/audit/enforcers/active-skill-resolver.d.ts +29 -0
- package/dist/src/services/audit/enforcers/active-skill-resolver.js +71 -0
- package/dist/src/services/audit/enforcers/design-draft-confirm.d.ts +25 -0
- package/dist/src/services/audit/enforcers/design-draft-confirm.js +54 -0
- package/dist/src/services/audit/enforcers/lint-audit-regression.d.ts +21 -0
- package/dist/src/services/audit/enforcers/lint-audit-regression.js +86 -0
- package/dist/src/services/audit/enforcers/lint-catalog-governance.d.ts +27 -0
- package/dist/src/services/audit/enforcers/lint-catalog-governance.js +38 -0
- package/dist/src/services/audit/enforcers/lint-cli-back.d.ts +16 -0
- package/dist/src/services/audit/enforcers/lint-cli-back.js +35 -0
- package/dist/src/services/audit/enforcers/lint-output-style.d.ts +11 -0
- package/dist/src/services/audit/enforcers/lint-output-style.js +94 -0
- package/dist/src/services/audit/enforcers/lint-reference-integrity.d.ts +6 -0
- package/dist/src/services/audit/enforcers/lint-reference-integrity.js +83 -0
- package/dist/src/services/audit/enforcers/lint-reference-shape.d.ts +30 -0
- package/dist/src/services/audit/enforcers/lint-reference-shape.js +272 -0
- package/dist/src/services/audit/enforcers/lint-style.d.ts +49 -0
- package/dist/src/services/audit/enforcers/lint-style.js +173 -0
- package/dist/src/services/audit/enforcers/lint-workflow-shape.d.ts +5 -0
- package/dist/src/services/audit/enforcers/lint-workflow-shape.js +141 -0
- package/dist/src/services/audit/enforcers/login-gate.d.ts +23 -0
- package/dist/src/services/audit/enforcers/login-gate.js +40 -0
- package/dist/src/services/audit/enforcers/mock-placement.d.ts +25 -0
- package/dist/src/services/audit/enforcers/mock-placement.js +48 -0
- package/dist/src/services/audit/enforcers/no-root-pollution.d.ts +21 -0
- package/dist/src/services/audit/enforcers/no-root-pollution.js +56 -0
- package/dist/src/services/audit/enforcers/pre-rd-scan.d.ts +22 -0
- package/dist/src/services/audit/enforcers/pre-rd-scan.js +23 -0
- package/dist/src/services/audit/enforcers/prototype-fidelity.d.ts +25 -0
- package/dist/src/services/audit/enforcers/prototype-fidelity.js +75 -0
- package/dist/src/services/audit/enforcers/resume-detection.d.ts +21 -0
- package/dist/src/services/audit/enforcers/resume-detection.js +52 -0
- package/dist/src/services/audit/enforcers/solo-code-ban.d.ts +23 -0
- package/dist/src/services/audit/enforcers/solo-code-ban.js +27 -0
- package/dist/src/services/audit/enforcers/sub-agent-sid.d.ts +25 -0
- package/dist/src/services/audit/enforcers/sub-agent-sid.js +63 -0
- package/dist/src/services/audit/enforcers/tech-doc-presence.d.ts +28 -0
- package/dist/src/services/audit/enforcers/tech-doc-presence.js +35 -0
- package/dist/src/services/audit/red-line-catalog-p2-a.d.ts +21 -0
- package/dist/src/services/audit/red-line-catalog-p2-a.js +233 -0
- package/dist/src/services/audit/red-line-catalog-p2-b.d.ts +19 -0
- package/dist/src/services/audit/red-line-catalog-p2-b.js +225 -0
- package/dist/src/services/audit/red-line-catalog.d.ts +51 -0
- package/dist/src/services/audit/red-line-catalog.js +210 -0
- package/dist/src/services/audit/red-lines-service.d.ts +23 -0
- package/dist/src/services/audit/red-lines-service.js +486 -0
- package/dist/src/services/audit/scanners/openspec-scanner.d.ts +15 -0
- package/dist/src/services/audit/scanners/openspec-scanner.js +55 -0
- package/dist/src/services/audit/scanners/rules-tree-scanner.d.ts +16 -0
- package/dist/src/services/audit/scanners/rules-tree-scanner.js +56 -0
- package/dist/src/services/audit/scanners/skills-tree-scanner.d.ts +17 -0
- package/dist/src/services/audit/scanners/skills-tree-scanner.js +46 -0
- package/dist/src/services/audit/static-service.d.ts +57 -0
- package/dist/src/services/audit/static-service.js +125 -0
- package/dist/src/services/audit/types.d.ts +69 -0
- package/dist/src/services/audit/types.js +13 -0
- package/dist/src/services/classify/classify-service.d.ts +42 -0
- package/dist/src/services/classify/classify-service.js +122 -0
- package/dist/src/services/classify/classify-types.d.ts +79 -0
- package/dist/src/services/classify/classify-types.js +90 -0
- package/dist/src/services/code-review/ocr-service.d.ts +129 -0
- package/dist/src/services/code-review/ocr-service.js +362 -0
- package/dist/src/services/config/config-migration.d.ts +32 -0
- package/dist/src/services/config/config-migration.js +111 -0
- package/dist/src/services/config/config-restore.d.ts +10 -0
- package/dist/src/services/config/config-restore.js +47 -0
- package/dist/src/services/config/config-rollback.d.ts +13 -0
- package/dist/src/services/config/config-rollback.js +26 -0
- package/dist/src/services/config/config-service.d.ts +36 -2
- package/dist/src/services/config/config-service.js +105 -0
- package/dist/src/services/config/config-types.d.ts +73 -0
- package/dist/src/services/config/config-types.js +28 -13
- package/dist/src/services/config/model-routing.js +5 -3
- package/dist/src/services/doctor/doctor-service.js +96 -0
- package/dist/src/services/ide/adapters/hermes-adapter.d.ts +21 -0
- package/dist/src/services/ide/adapters/hermes-adapter.js +51 -0
- package/dist/src/services/ide/adapters/openclaw-adapter.d.ts +14 -0
- package/dist/src/services/ide/adapters/openclaw-adapter.js +42 -0
- package/dist/src/services/ide/ide-registry.js +7 -0
- package/dist/src/services/ide/ide-types.d.ts +1 -1
- package/dist/src/services/openspec/openspec-propose-from-doctor-service.d.ts +31 -0
- package/dist/src/services/openspec/openspec-propose-from-doctor-service.js +95 -0
- package/dist/src/services/preferences/preferences-service.d.ts +6 -0
- package/dist/src/services/preferences/preferences-service.js +43 -0
- package/dist/src/services/preferences/preferences-types.d.ts +90 -0
- package/dist/src/services/preferences/preferences-types.js +38 -0
- package/dist/src/services/rd/rd-service.js +29 -1
- package/dist/src/services/skills/skill-conformance-service.d.ts +40 -0
- package/dist/src/services/skills/skill-conformance-service.js +136 -0
- package/dist/src/services/skills/skill-runbook-service.js +44 -10
- package/dist/src/services/skills/sync-service.d.ts +86 -0
- package/dist/src/services/skills/sync-service.js +271 -0
- package/dist/src/services/slice/slice-check-service.js +166 -13
- package/dist/src/services/slice/slice-check-types.d.ts +1 -1
- package/dist/src/services/standards/migrate-claude-rules-service.d.ts +19 -0
- package/dist/src/services/standards/migrate-claude-rules-service.js +193 -0
- package/dist/src/services/understand/understand-scan-service.js +15 -2
- package/dist/src/services/understand/understand-types.d.ts +26 -0
- package/dist/src/services/upgrade/1x-detector-service.d.ts +7 -0
- package/dist/src/services/upgrade/1x-detector-service.js +94 -0
- package/dist/src/services/upgrade/gitignore-migrate-service.d.ts +56 -0
- package/dist/src/services/upgrade/gitignore-migrate-service.js +170 -0
- package/dist/src/services/upgrade/upgrade-service.d.ts +47 -0
- package/dist/src/services/upgrade/upgrade-service.js +381 -0
- package/dist/src/services/workflow/workflow-router-service.js +15 -4
- package/dist/src/services/workspace/claude-settings-template.d.ts +53 -0
- package/dist/src/services/workspace/claude-settings-template.js +133 -0
- package/dist/src/services/workspace/sid-naming-guard.d.ts +14 -0
- package/dist/src/services/workspace/sid-naming-guard.js +31 -0
- package/dist/src/services/workspace/workspace-archive-service.d.ts +19 -0
- package/dist/src/services/workspace/workspace-archive-service.js +32 -0
- package/dist/src/services/workspace/workspace-clean-service.d.ts +41 -0
- package/dist/src/services/workspace/workspace-clean-service.js +86 -0
- package/dist/src/services/workspace/workspace-service.d.ts +24 -0
- package/dist/src/services/workspace/workspace-service.js +124 -2
- package/dist/src/services/workspace/workspace-state-service.d.ts +7 -0
- package/dist/src/services/workspace/workspace-state-service.js +43 -0
- package/dist/src/shared/change-id.js +4 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +8 -2
- package/schemas/doctor-report.schema.json +1 -1
- package/scripts/install-skills.mjs +296 -12
- package/skills/peaks-doctor/SKILL.md +59 -0
- package/skills/peaks-doctor/references/doctor-check-catalog.md +31 -0
- package/skills/peaks-doctor/references/from-doctor-flow.md +64 -0
- package/skills/peaks-doctor/test_prompts.json +17 -0
- package/skills/peaks-ide/SKILL.md +2 -0
- package/skills/peaks-qa/SKILL.md +9 -7
- package/skills/peaks-qa/references/artifact-per-request.md +19 -5
- package/skills/peaks-qa/references/qa-perf-test-plan.md +6 -6
- package/skills/peaks-qa/references/qa-runbook.md +1 -1
- package/skills/peaks-rd/SKILL.md +25 -10
- package/skills/peaks-rd/references/ocr-integration.md +214 -0
- package/skills/peaks-rd/references/rd-fanout-contracts.md +70 -0
- package/skills/peaks-rd/references/rd-runbook.md +1 -1
- package/skills/peaks-solo/SKILL.md +16 -4
- package/skills/peaks-solo/references/anchoring-and-session-info.md +9 -0
- package/skills/peaks-solo/references/step-0-55-1x-detection.md +82 -0
- package/skills/peaks-solo/references/workflow-gates-and-types.md +9 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peaks classify CLI (Slice L1a + L1b).
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* - peaks classify run --project <path> [--override <level> --reason "<text>"]
|
|
6
|
+
* Classify the current diff via the heuristic + return a JSON envelope
|
|
7
|
+
* with the chosen level, gate set, and audit log.
|
|
8
|
+
* - peaks classify override --level <level> --reason "<text>" --project <path>
|
|
9
|
+
* Force a level; writes the override to the audit log.
|
|
10
|
+
* - peaks classify upgrade --level <level> --reason "<text>" --project <path>
|
|
11
|
+
* Same as override but explicitly framed as an upgrade (audit log
|
|
12
|
+
* records the upgrade event separately from override).
|
|
13
|
+
*
|
|
14
|
+
* Downgrade is REFUSED (per spec §4: "peaks classify downgrade" always
|
|
15
|
+
* errors out). LLM may ask; the CLI never grants.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { classifyTask } from '../../services/classify/classify-service.js';
|
|
20
|
+
import { TASK_LEVELS } from '../../services/classify/classify-types.js';
|
|
21
|
+
import { loadPreferences } from '../../services/preferences/preferences-service.js';
|
|
22
|
+
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
23
|
+
import { fail, ok } from '../../shared/result.js';
|
|
24
|
+
const CLASSIFY_AUDIT_FILE = 'classify-audit.jsonl';
|
|
25
|
+
function getSignalsFromGitDiff(projectRoot) {
|
|
26
|
+
// Use git diff --stat to extract file count + line count. Fall back to
|
|
27
|
+
// zeros if git is unavailable (e.g. fresh repo with no commits).
|
|
28
|
+
let stdout;
|
|
29
|
+
try {
|
|
30
|
+
const { execFileSync } = require('node:child_process');
|
|
31
|
+
stdout = execFileSync('git', ['diff', '--shortstat', 'HEAD'], {
|
|
32
|
+
cwd: projectRoot,
|
|
33
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
34
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
35
|
+
}).toString('utf8');
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return { filesChanged: 0, linesChanged: 0, touchesDependencies: false, touchesMigrationScripts: false, isPureRefactor: true, keywords: [] };
|
|
39
|
+
}
|
|
40
|
+
const lines = stdout.split('\n').filter((l) => l.trim().length > 0);
|
|
41
|
+
const filesChanged = lines.length;
|
|
42
|
+
let added = 0;
|
|
43
|
+
let removed = 0;
|
|
44
|
+
let touchesDependencies = false;
|
|
45
|
+
let touchesMigrationScripts = false;
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
const match = /(\d+)\s+insertion.*?(\d+)\s+deletion/.exec(line);
|
|
48
|
+
if (match) {
|
|
49
|
+
added += Number(match[1]);
|
|
50
|
+
removed += Number(match[2]);
|
|
51
|
+
}
|
|
52
|
+
if (/(package\.json|pnpm-lock\.yaml|yarn\.lock|requirements\.txt|go\.mod)/.test(line)) {
|
|
53
|
+
touchesDependencies = true;
|
|
54
|
+
}
|
|
55
|
+
if (/(migrate|codemod|backfill|schema)/.test(line)) {
|
|
56
|
+
touchesMigrationScripts = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// isPureRefactor: heuristic — if added lines / removed lines < 0.1 OR
|
|
60
|
+
// no new exports were added, treat as refactor. For L2.2 the signal is
|
|
61
|
+
// binary (true/false). Default: true (no behavior change is the safe
|
|
62
|
+
// assumption; flip to false when keyword 'add' / 'new' / 'feature' present).
|
|
63
|
+
const isPureRefactor = true;
|
|
64
|
+
return {
|
|
65
|
+
filesChanged,
|
|
66
|
+
linesChanged: added + removed,
|
|
67
|
+
touchesDependencies,
|
|
68
|
+
touchesMigrationScripts,
|
|
69
|
+
isPureRefactor,
|
|
70
|
+
keywords: [],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function appendAuditEntry(projectRoot, entry) {
|
|
74
|
+
const auditDir = join(projectRoot, '.peaks/_runtime');
|
|
75
|
+
if (!existsSync(auditDir)) {
|
|
76
|
+
try {
|
|
77
|
+
mkdirSync(auditDir, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
catch { /* ignore */ }
|
|
80
|
+
}
|
|
81
|
+
const auditPath = join(auditDir, CLASSIFY_AUDIT_FILE);
|
|
82
|
+
let body = '';
|
|
83
|
+
try {
|
|
84
|
+
if (existsSync(auditPath)) {
|
|
85
|
+
body = readFileSync(auditPath, 'utf8');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch { /* ignore */ }
|
|
89
|
+
body += JSON.stringify(entry) + '\n';
|
|
90
|
+
try {
|
|
91
|
+
writeFileSync(auditPath, body);
|
|
92
|
+
}
|
|
93
|
+
catch { /* best-effort */ }
|
|
94
|
+
}
|
|
95
|
+
function isTaskLevel(value) {
|
|
96
|
+
return TASK_LEVELS.includes(value);
|
|
97
|
+
}
|
|
98
|
+
export function registerClassifyCommands(program, io) {
|
|
99
|
+
const classify = program
|
|
100
|
+
.command('classify')
|
|
101
|
+
.description('L1a task classification: 5-level heuristic (typo/bug/feature/refactor/migration) + override/upgrade + audit log');
|
|
102
|
+
addJsonOption(classify
|
|
103
|
+
.command('run')
|
|
104
|
+
.description('Classify the current diff (git diff HEAD) into one of 5 task levels')
|
|
105
|
+
.requiredOption('--project <path>', 'target project root')
|
|
106
|
+
.option('--override <level>', 'force a level (one of typo|bug|feature|refactor|migration); requires --reason')
|
|
107
|
+
.option('--reason <text>', 'reason for the override (mandatory when --override is set)')).action(async (options) => {
|
|
108
|
+
try {
|
|
109
|
+
const prefs = await loadPreferences(options.project);
|
|
110
|
+
const signals = getSignalsFromGitDiff(options.project);
|
|
111
|
+
let override;
|
|
112
|
+
if (options.override !== undefined) {
|
|
113
|
+
if (!isTaskLevel(options.override)) {
|
|
114
|
+
printResult(io, fail('classify.run', 'INVALID_LEVEL', `level must be one of: ${TASK_LEVELS.join(', ')}`, { provided: options.override }, ['Pass one of typo, bug, feature, refactor, migration']), options.json);
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (options.reason === undefined || options.reason.length === 0) {
|
|
119
|
+
printResult(io, fail('classify.run', 'REASON_REQUIRED', '--reason is required when --override is set', {}, ['Provide a non-empty reason for the override']), options.json);
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
override = { level: options.override, reason: options.reason };
|
|
124
|
+
}
|
|
125
|
+
const result = classifyTask(override !== undefined
|
|
126
|
+
? { signals, conservatism: prefs.classifyConservatism, override }
|
|
127
|
+
: { signals, conservatism: prefs.classifyConservatism }, prefs.classifyRules.feature_threshold_files, prefs.classifyRules.feature_threshold_lines);
|
|
128
|
+
appendAuditEntry(options.project, result.audit);
|
|
129
|
+
printResult(io, ok('classify.run', result, [], [
|
|
130
|
+
`gate set for level "${result.level}": ${result.gateSet.stages.join(', ')}`,
|
|
131
|
+
`audit log: .peaks/_runtime/${CLASSIFY_AUDIT_FILE}`,
|
|
132
|
+
]), options.json);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
printResult(io, fail('classify.run', 'CLASSIFY_RUN_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Run peaks classify --help for usage']), options.json);
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
// Downgrade is REFUSED per spec §4. Surface this as a hard fail.
|
|
140
|
+
classify
|
|
141
|
+
.command('downgrade')
|
|
142
|
+
.description('REFUSED per spec §4 — peaks-cli never downgrades a classification; ask the user to override explicitly')
|
|
143
|
+
.requiredOption('--level <level>', 'attempted level')
|
|
144
|
+
.requiredOption('--reason <text>', 'reason for the attempt (always rejected)')
|
|
145
|
+
.requiredOption('--project <path>', 'target project root')
|
|
146
|
+
.option('--json', 'print machine-readable JSON envelope')
|
|
147
|
+
.action(async (options) => {
|
|
148
|
+
printResult(io, fail('classify.downgrade', 'DOWNGRADE_REFUSED', 'peaks classify downgrade is refused per spec §4. Use --override (with reason) on `classify run` to force a level; the CLI never downgrades a classification unilaterally.', { attemptedLevel: options.level, reason: options.reason }, ['Use `peaks classify run --override <level> --reason "<text>"` instead']), options.json);
|
|
149
|
+
process.exitCode = 2;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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-cli
|
|
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-cli 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-cli command, not
|
|
30
|
+
* an ocr command).
|
|
31
|
+
*/
|
|
32
|
+
import { Command } from 'commander';
|
|
33
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
34
|
+
export declare function registerCodeReviewCommands(program: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { getOcrConfigTemplate, detectOcr, runOcrReview } from '../../services/code-review/ocr-service.js';
|
|
2
|
+
import { getOcrLlmConfig, getUserConfigPath, redactConfigSecrets } from '../../services/config/config-service.js';
|
|
3
|
+
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
4
|
+
import { fail, ok } from '../../shared/result.js';
|
|
5
|
+
export function registerCodeReviewCommands(program, io) {
|
|
6
|
+
const codeReview = program
|
|
7
|
+
.command('code-review')
|
|
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 ships as a peaks-cli dependency (not optional). 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
|
+
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();
|
|
14
|
+
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]);
|
|
23
|
+
printResult(io, envelope, options.json);
|
|
24
|
+
if (detect.state !== 'ready') {
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
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;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
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-cli `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.
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
addJsonOption(codeReview
|
|
64
|
+
.command('config-template')
|
|
65
|
+
.description('Print the JSON snippet the user should paste into their peaks-cli config (`peaksConfig.ocr.llm`). This command does NOT write anything. The user is the only party that touches the LLM token / URL / model — peaks-cli 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-cli 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-cli 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);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { executeMigration, planMigration } from '../../services/config/config-migration.js';
|
|
1
2
|
import { getConfig, getMiniMaxProviderConfig, getMiniMaxProviderStatus, isSensitiveConfigPath, redactConfigSecrets, setConfig, setMiniMaxProviderConfig } from '../../services/config/config-service.js';
|
|
3
|
+
import { listAvailableFields, restoreField } from '../../services/config/config-restore.js';
|
|
4
|
+
import { executeRollback, planRollback } from '../../services/config/config-rollback.js';
|
|
2
5
|
import { testMiniMaxProvider } from '../../services/providers/minimax-provider-service.js';
|
|
3
6
|
import { fail, ok } from '../../shared/result.js';
|
|
4
7
|
import { addJsonOption, getErrorMessage, isMiniMaxHttpsUrl, parseConfigLayer, printInvalidConfigLayer, printResult, redactSensitiveErrorMessage, summarizeMiniMaxSmokeResult } from '../cli-helpers.js';
|
|
5
8
|
export function registerConfigCommands(program, io) {
|
|
6
9
|
const config = program.command('config').description('Manage Peaks configuration');
|
|
7
10
|
registerConfigGetSetCommands(config, io);
|
|
11
|
+
registerConfigMigrationCommands(config, io);
|
|
8
12
|
registerMiniMaxProviderCommands(config, io);
|
|
9
13
|
}
|
|
10
14
|
function registerConfigGetSetCommands(config, io) {
|
|
@@ -44,6 +48,92 @@ function registerConfigGetSetCommands(config, io) {
|
|
|
44
48
|
}
|
|
45
49
|
});
|
|
46
50
|
}
|
|
51
|
+
function registerConfigMigrationCommands(config, io) {
|
|
52
|
+
// Slice 0.5 Task 14 — peaks config {migrate,rollback,restore} subcommands.
|
|
53
|
+
// Wires up the config-migration / config-rollback / config-restore services
|
|
54
|
+
// from Tasks 10-12. The default mode is dry-run; --apply writes.
|
|
55
|
+
config
|
|
56
|
+
.command('migrate')
|
|
57
|
+
.description('Migrate global config from 1.x to 2.0 (YAGNI slim + per-project fields)')
|
|
58
|
+
.option('--project <path>', 'current project root (for migrating per-project fields)', process.cwd())
|
|
59
|
+
.option('--apply', 'actually write changes (default is dry-run)')
|
|
60
|
+
.option('--dry-run', 'plan only, do not write (default)')
|
|
61
|
+
.option('--json', 'JSON envelope output')
|
|
62
|
+
.action((options) => {
|
|
63
|
+
try {
|
|
64
|
+
const apply = options.apply === true;
|
|
65
|
+
if (apply) {
|
|
66
|
+
const result = executeMigration({ currentProjectRoot: options.project, apply: true });
|
|
67
|
+
printResult(io, ok('config.migrate', result), options.json);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const plan = planMigration({ currentProjectRoot: options.project });
|
|
71
|
+
printResult(io, ok('config.migrate', { ...plan, applied: false }), options.json);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
printResult(io, fail('config.migrate', 'CONFIG_MIGRATE_FAILED', getErrorMessage(error), {}, ['Inspect ~/.peaks/config.json and re-run with --apply']), options.json);
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
config
|
|
79
|
+
.command('rollback')
|
|
80
|
+
.description('Rollback global config to 1.x from .bak')
|
|
81
|
+
.option('--apply', 'actually write changes (default is dry-run)')
|
|
82
|
+
.option('--dry-run', 'plan only, do not write (default)')
|
|
83
|
+
.option('--json', 'JSON envelope output')
|
|
84
|
+
.action((options) => {
|
|
85
|
+
try {
|
|
86
|
+
const apply = options.apply === true;
|
|
87
|
+
if (apply) {
|
|
88
|
+
const result = executeRollback({ apply: true });
|
|
89
|
+
printResult(io, ok('config.rollback', result), options.json);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const plan = planRollback();
|
|
93
|
+
printResult(io, ok('config.rollback', { ...plan, applied: false }), options.json);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const message = getErrorMessage(error);
|
|
97
|
+
const code = message.startsWith('NO_BACKUP') ? 'NO_BACKUP' : 'CONFIG_ROLLBACK_FAILED';
|
|
98
|
+
io.stderr(`${code}: ${message}`);
|
|
99
|
+
printResult(io, fail('config.rollback', code, message, {}, ['Re-run peaks config migrate --apply to recreate the .bak']), options.json);
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
config
|
|
104
|
+
.command('restore')
|
|
105
|
+
.description('Restore a single archived field from .bak to a sidecar file')
|
|
106
|
+
.option('--field <name>', 'field name to restore (e.g. language, currentWorkspace)')
|
|
107
|
+
.option('--list', 'list all fields available in .bak')
|
|
108
|
+
.option('--apply', 'actually write sidecar (default is dry-run)')
|
|
109
|
+
.option('--dry-run', 'plan only, do not write (default)')
|
|
110
|
+
.option('--json', 'JSON envelope output')
|
|
111
|
+
.action((options) => {
|
|
112
|
+
try {
|
|
113
|
+
if (options.list === true || !options.field) {
|
|
114
|
+
const fields = listAvailableFields();
|
|
115
|
+
printResult(io, ok('config.restore', { fields, applied: false }), options.json);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const apply = options.apply === true;
|
|
119
|
+
const result = restoreField({ field: options.field, apply });
|
|
120
|
+
printResult(io, ok('config.restore', result), options.json);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
const message = getErrorMessage(error);
|
|
124
|
+
let code = 'CONFIG_RESTORE_FAILED';
|
|
125
|
+
if (message.startsWith('NO_BACKUP'))
|
|
126
|
+
code = 'NO_BACKUP';
|
|
127
|
+
else if (message.startsWith('RESTORE_GUARDED'))
|
|
128
|
+
code = 'RESTORE_GUARDED';
|
|
129
|
+
else if (message.startsWith('FIELD_NOT_FOUND'))
|
|
130
|
+
code = 'FIELD_NOT_FOUND';
|
|
131
|
+
io.stderr(`${code}: ${message}`);
|
|
132
|
+
printResult(io, fail('config.restore', code, message, {}, ['Use --list to see available fields']), options.json);
|
|
133
|
+
process.exitCode = 1;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
47
137
|
function printConfigSetError(io, error, asJson) {
|
|
48
138
|
const message = getErrorMessage(error);
|
|
49
139
|
if (message === 'Sensitive config keys must be stored in the user config layer') {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peaks context * CLI (Slice #3 L1c) — context 4-layer loader.
|
|
3
|
+
*
|
|
4
|
+
* Per docs/superpowers/specs/2026-06-11-peaks-cli-l1-l2-l3-redesign.md §4 L1c:
|
|
5
|
+
* - L0: full read (every SKILL.md in full)
|
|
6
|
+
* - L1: summary (one paragraph per skill + the skill list)
|
|
7
|
+
* - L2: index (just names + paths)
|
|
8
|
+
* - L3: fuzzy search by query (delegates to peaks memory search)
|
|
9
|
+
*
|
|
10
|
+
* The LLM-side UX layer (peaks-solo / peaks-ide) picks the layer based
|
|
11
|
+
* on the L1a task level:
|
|
12
|
+
* - typo: L0 only (a few lines)
|
|
13
|
+
* - bug: L0 + L1
|
|
14
|
+
* - feature: L0 + L1 + L2 + L3(按需)
|
|
15
|
+
* - refactor: L0 + L1 + L2 + L3
|
|
16
|
+
* - migration: L0 + L1 + L2 + L3 + codegraph
|
|
17
|
+
*/
|
|
18
|
+
import { Command } from 'commander';
|
|
19
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
20
|
+
export type ContextLayer = 'L0' | 'L1' | 'L2' | 'L3';
|
|
21
|
+
export declare function registerContextCommands(program: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peaks context * CLI (Slice #3 L1c) — context 4-layer loader.
|
|
3
|
+
*
|
|
4
|
+
* Per docs/superpowers/specs/2026-06-11-peaks-cli-l1-l2-l3-redesign.md §4 L1c:
|
|
5
|
+
* - L0: full read (every SKILL.md in full)
|
|
6
|
+
* - L1: summary (one paragraph per skill + the skill list)
|
|
7
|
+
* - L2: index (just names + paths)
|
|
8
|
+
* - L3: fuzzy search by query (delegates to peaks memory search)
|
|
9
|
+
*
|
|
10
|
+
* The LLM-side UX layer (peaks-solo / peaks-ide) picks the layer based
|
|
11
|
+
* on the L1a task level:
|
|
12
|
+
* - typo: L0 only (a few lines)
|
|
13
|
+
* - bug: L0 + L1
|
|
14
|
+
* - feature: L0 + L1 + L2 + L3(按需)
|
|
15
|
+
* - refactor: L0 + L1 + L2 + L3
|
|
16
|
+
* - migration: L0 + L1 + L2 + L3 + codegraph
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { searchMemory } from '../../services/memory/memory-search-service.js';
|
|
21
|
+
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
22
|
+
import { fail, ok } from '../../shared/result.js';
|
|
23
|
+
const SKILLS_DIR = 'skills';
|
|
24
|
+
function readSkillsDir(projectRoot) {
|
|
25
|
+
const skillsRoot = join(projectRoot, SKILLS_DIR);
|
|
26
|
+
if (!existsSync(skillsRoot))
|
|
27
|
+
return [];
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const entry of readdirSync(skillsRoot, { withFileTypes: true })) {
|
|
30
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
31
|
+
continue;
|
|
32
|
+
const skillMd = join(skillsRoot, entry.name, 'SKILL.md');
|
|
33
|
+
if (existsSync(skillMd)) {
|
|
34
|
+
out.push(`skills/${entry.name}/SKILL.md`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
function readSkillFile(projectRoot, relPath) {
|
|
40
|
+
const abs = join(projectRoot, relPath);
|
|
41
|
+
try {
|
|
42
|
+
return readFileSync(abs, 'utf-8');
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function fetchL0(projectRoot) {
|
|
49
|
+
const files = readSkillsDir(projectRoot);
|
|
50
|
+
const warnings = [];
|
|
51
|
+
let content = '';
|
|
52
|
+
let byteSize = 0;
|
|
53
|
+
const sizes = [];
|
|
54
|
+
for (const rel of files) {
|
|
55
|
+
const body = readSkillFile(projectRoot, rel);
|
|
56
|
+
if (body.length === 0) {
|
|
57
|
+
warnings.push(`failed to read ${rel}`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
content += `## ${rel}\n\n${body}\n\n`;
|
|
61
|
+
sizes.push({ path: rel, bytes: body.length });
|
|
62
|
+
byteSize += body.length;
|
|
63
|
+
}
|
|
64
|
+
return { layer: 'L0', description: 'full read: every SKILL.md concatenated', files: sizes, content, byteSize, warnings };
|
|
65
|
+
}
|
|
66
|
+
async function fetchL1(projectRoot) {
|
|
67
|
+
const files = readSkillsDir(projectRoot);
|
|
68
|
+
const warnings = [];
|
|
69
|
+
let content = '';
|
|
70
|
+
let byteSize = 0;
|
|
71
|
+
const sizes = [];
|
|
72
|
+
for (const rel of files) {
|
|
73
|
+
const body = readSkillFile(projectRoot, rel);
|
|
74
|
+
if (body.length === 0) {
|
|
75
|
+
warnings.push(`failed to read ${rel}`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const firstPara = body.split(/\r?\n\r?\n/, 2)[0] ?? body.slice(0, 400);
|
|
79
|
+
content += `## ${rel}\n\n${firstPara}\n\n`;
|
|
80
|
+
sizes.push({ path: rel, bytes: firstPara.length });
|
|
81
|
+
byteSize += firstPara.length;
|
|
82
|
+
}
|
|
83
|
+
return { layer: 'L1', description: 'summary: first paragraph per skill', files: sizes, content, byteSize, warnings };
|
|
84
|
+
}
|
|
85
|
+
async function fetchL2(projectRoot) {
|
|
86
|
+
const files = readSkillsDir(projectRoot);
|
|
87
|
+
const content = files.map((f) => `- ${f}`).join('\n') + '\n';
|
|
88
|
+
const byteSize = content.length;
|
|
89
|
+
return {
|
|
90
|
+
layer: 'L2',
|
|
91
|
+
description: 'index: skill paths only (no body)',
|
|
92
|
+
files: files.map((f) => ({ path: f, bytes: 0 })),
|
|
93
|
+
content,
|
|
94
|
+
byteSize,
|
|
95
|
+
warnings: [],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async function fetchL3(projectRoot, query) {
|
|
99
|
+
const warnings = [];
|
|
100
|
+
if (query.trim().length === 0) {
|
|
101
|
+
warnings.push('empty query; L3 requires a --query string for fuzzy search');
|
|
102
|
+
return { layer: 'L3', description: 'fuzzy search (empty query)', files: [], content: '', byteSize: 0, warnings };
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const hits = searchMemory({ projectRoot, query, limit: 10 });
|
|
106
|
+
const lines = [];
|
|
107
|
+
for (const hit of hits) {
|
|
108
|
+
lines.push(`- [${hit.score.toFixed(2)}] ${hit.sourcePath}: ${hit.description.slice(0, 120)}`);
|
|
109
|
+
}
|
|
110
|
+
const content = lines.join('\n') + '\n';
|
|
111
|
+
return {
|
|
112
|
+
layer: 'L3',
|
|
113
|
+
description: 'fuzzy search: top 10 hits from memory index',
|
|
114
|
+
files: hits.map((h) => ({ path: h.sourcePath, bytes: h.description.length })),
|
|
115
|
+
content,
|
|
116
|
+
byteSize: content.length,
|
|
117
|
+
warnings,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return {
|
|
122
|
+
layer: 'L3',
|
|
123
|
+
description: 'fuzzy search (failed)',
|
|
124
|
+
files: [],
|
|
125
|
+
content: '',
|
|
126
|
+
byteSize: 0,
|
|
127
|
+
warnings: [`L3 search failed: ${getErrorMessage(error)}`],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export function registerContextCommands(program, io) {
|
|
132
|
+
const context = program
|
|
133
|
+
.command('context')
|
|
134
|
+
.description('Slice L1c: context 4-layer loader (L0 full / L1 summary / L2 index / L3 fuzzy)');
|
|
135
|
+
addJsonOption(context
|
|
136
|
+
.command('layer')
|
|
137
|
+
.description('Fetch a context layer (L0 = full SKILL.md; L1 = first paragraph; L2 = index; L3 = fuzzy search by --query)')
|
|
138
|
+
.requiredOption('--project <path>', 'target project root')
|
|
139
|
+
.requiredOption('--level <L0|L1|L2|L3>', 'context layer to load')
|
|
140
|
+
.option('--query <text>', 'fuzzy search query (required for L3)')).action(async (options) => {
|
|
141
|
+
try {
|
|
142
|
+
const level = options.level;
|
|
143
|
+
if (level !== 'L0' && level !== 'L1' && level !== 'L2' && level !== 'L3') {
|
|
144
|
+
printResult(io, fail('context.layer', 'INVALID_LEVEL', `level must be one of L0, L1, L2, L3 (got ${level})`, { provided: level }, ['Pass --level L0|L1|L2|L3']), options.json);
|
|
145
|
+
process.exitCode = 1;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
let payload;
|
|
149
|
+
if (level === 'L0')
|
|
150
|
+
payload = await fetchL0(options.project);
|
|
151
|
+
else if (level === 'L1')
|
|
152
|
+
payload = await fetchL1(options.project);
|
|
153
|
+
else if (level === 'L2')
|
|
154
|
+
payload = await fetchL2(options.project);
|
|
155
|
+
else
|
|
156
|
+
payload = await fetchL3(options.project, options.query ?? '');
|
|
157
|
+
printResult(io, ok('context.layer', payload, [], [
|
|
158
|
+
`${payload.layer} loaded: ${payload.byteSize} bytes across ${payload.files.length} file(s)`,
|
|
159
|
+
payload.warnings.length > 0 ? `${payload.warnings.length} warning(s); see envelope.warnings` : null
|
|
160
|
+
].filter((x) => typeof x === 'string')), options.json);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
printResult(io, fail('context.layer', 'CONTEXT_LAYER_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Verify the project path and --level value']), options.json);
|
|
164
|
+
process.exitCode = 1;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
@@ -6,10 +6,12 @@ import { executeProjectMemoryBackup, executeProjectMemoryExtract, summarizeProje
|
|
|
6
6
|
import { summarizeProjectStandardsInitResult, summarizeProjectStandardsUpdateResult } from '../../services/standards/project-standards-service.js';
|
|
7
7
|
import { executeProjectStandardsInitIdeAware, executeProjectStandardsUpdateIdeAware } from '../../services/standards/ide-aware-standards-service.js';
|
|
8
8
|
import { migrateStandards } from '../../services/standards/migrate-service.js';
|
|
9
|
+
import { migrateClaudeRules } from '../../services/standards/migrate-claude-rules-service.js';
|
|
9
10
|
import { listProfiles } from '../../services/profiles/profile-service.js';
|
|
10
11
|
import { planProxyTest } from '../../services/proxy/proxy-service.js';
|
|
11
12
|
import { runDoctor } from '../../services/doctor/doctor-service.js';
|
|
12
13
|
import { listSkills } from '../../services/skills/skill-registry.js';
|
|
14
|
+
import { runSkillSync, SYNC_PLATFORMS } from '../../services/skills/sync-service.js';
|
|
13
15
|
import { inspectSkillRunbook } from '../../services/skills/skill-runbook-service.js';
|
|
14
16
|
import { setSkillPresence, clearSkillPresence, getSkillPresence, isSkillPresenceMode, touchSkillHeartbeat } from '../../services/skills/skill-presence-service.js';
|
|
15
17
|
import { detectPresenceMarker } from '../../services/hooks/presence-marker-detector.js';
|
|
@@ -88,6 +90,42 @@ export function registerCoreAndArtifactCommands(program, io) {
|
|
|
88
90
|
process.exitCode = 1;
|
|
89
91
|
}
|
|
90
92
|
});
|
|
93
|
+
// Slice #12 final piece (per spec §9 line 1105):
|
|
94
|
+
// `peaks skills sync 8 平台分发`. Idempotent: re-running is a
|
|
95
|
+
// no-op when the symlinks are already correct.
|
|
96
|
+
addJsonOption(skill
|
|
97
|
+
.command('sync')
|
|
98
|
+
.description(`Sync the peaks-* skill family to one or all of the 8 supported LLM-CLI platforms (${SYNC_PLATFORMS.join(', ')}). Idempotent.`)
|
|
99
|
+
.option('--platform <id>', `sync only one platform (default: --all). Valid: ${SYNC_PLATFORMS.join(', ')}`)
|
|
100
|
+
.option('--all', 'sync all 8 platforms (default if --platform is omitted)')
|
|
101
|
+
.option('--dry-run', 'do not write; emit the same shape with applied=false')
|
|
102
|
+
.option('--project <path>', 'project root (default: cwd)')).action(async (options) => {
|
|
103
|
+
try {
|
|
104
|
+
const projectRoot = options.project ?? process.cwd();
|
|
105
|
+
const platforms = options.platform !== undefined ? [options.platform] : undefined;
|
|
106
|
+
const result = await runSkillSync({
|
|
107
|
+
projectRoot,
|
|
108
|
+
...(platforms !== undefined ? { platforms } : {}),
|
|
109
|
+
...(options.dryRun === true ? { dryRun: true } : {}),
|
|
110
|
+
});
|
|
111
|
+
const envelope = ok('skill.sync', result, [], [
|
|
112
|
+
`syncedCount: ${result.syncedCount}/${result.perPlatform.length} platforms`,
|
|
113
|
+
`totalInstalled: ${result.totalInstalled} skill symlinks`,
|
|
114
|
+
result.failedCount > 0
|
|
115
|
+
? `failedCount: ${result.failedCount} (run \`peaks skill status\` for details)`
|
|
116
|
+
: 'no failures',
|
|
117
|
+
]);
|
|
118
|
+
printResult(io, envelope, options.json);
|
|
119
|
+
if (result.failedCount > 0) {
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
const message = getErrorMessage(error);
|
|
125
|
+
printResult(io, fail('skill.sync', 'SKILL_SYNC_FAILED', message, { applied: false }, [message]), options.json);
|
|
126
|
+
process.exitCode = 1;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
91
129
|
addJsonOption(skill
|
|
92
130
|
.command('runbook <name>')
|
|
93
131
|
.description('Inspect a skill Default runbook section and its --apply authorization-note status')).action(async (name, options) => {
|
|
@@ -463,10 +501,30 @@ export function registerCoreAndArtifactCommands(program, io) {
|
|
|
463
501
|
});
|
|
464
502
|
addJsonOption(standards
|
|
465
503
|
.command('migrate')
|
|
466
|
-
.description('Rewrite a consumer project CLAUDE.md to drop the legacy heartbeat block (slice 028). Dry-run by default; pass --apply to write.')
|
|
504
|
+
.description('Rewrite a consumer project CLAUDE.md to drop the legacy heartbeat block (slice 028). Dry-run by default; pass --apply to write. With --from-claude-rules, thins the 1.x .claude/rules/ tree to 2-line pointers and scaffolds .peaks/standards/ (slice 2026-06-12-standards-migrate-claude-rules).')
|
|
467
505
|
.option('--project <path>', 'target project root')
|
|
468
|
-
.option('--apply', 'rewrite the legacy block in place; default is dry-run')
|
|
506
|
+
.option('--apply', 'rewrite the legacy block in place; default is dry-run')
|
|
507
|
+
.option('--from-claude-rules', 'thin .claude/rules/ to pointers and scaffold .peaks/standards/ (used by `peaks upgrade --to 2.0`)')).action((options) => {
|
|
469
508
|
const projectRoot = options.project ?? process.cwd();
|
|
509
|
+
if (options.fromClaudeRules === true) {
|
|
510
|
+
try {
|
|
511
|
+
const result = migrateClaudeRules({ projectRoot, apply: options.apply === true });
|
|
512
|
+
printResult(io, ok('standards.migrate', result.data, [], [...result.data.nextActions]), options.json);
|
|
513
|
+
}
|
|
514
|
+
catch (error) {
|
|
515
|
+
printResult(io, fail('standards.migrate', 'STANDARDS_MIGRATE_FAILED', getErrorMessage(error), {
|
|
516
|
+
backupPath: null,
|
|
517
|
+
thinnedFiles: [],
|
|
518
|
+
scaffoldedFiles: [],
|
|
519
|
+
preservedFiles: [],
|
|
520
|
+
wouldChange: false,
|
|
521
|
+
applied: false,
|
|
522
|
+
nextActions: [],
|
|
523
|
+
}, [getErrorMessage(error)]), options.json);
|
|
524
|
+
process.exitCode = 1;
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
470
528
|
try {
|
|
471
529
|
const result = migrateStandards({ project: projectRoot, apply: options.apply === true });
|
|
472
530
|
printResult(io, ok('standards.migrate', result.data, [], result.data.nextActions), options.json);
|