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
|
@@ -2,6 +2,10 @@ import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
|
2
2
|
import { detectIdeFromContext, parseAdapterStdin, parseClaudeShapeStdin, pluckObject, pluckString } from '../../services/ide/hook-translator.js';
|
|
3
3
|
import { buildCanonicalHook, formatDecisionResponse } from '../../services/ide/hook-protocol.js';
|
|
4
4
|
import { getAdapter } from '../../services/ide/ide-registry.js';
|
|
5
|
+
import { evaluateSoloCodeBan } from '../../services/audit/enforcers/solo-code-ban.js';
|
|
6
|
+
import { isRootWrite } from '../../services/audit/enforcers/no-root-pollution.js';
|
|
7
|
+
import { checkLoginGate } from '../../services/audit/enforcers/login-gate.js';
|
|
8
|
+
import { resolveActiveSkillForCaller } from '../../services/audit/enforcers/active-skill-resolver.js';
|
|
5
9
|
import { fail, ok } from '../../shared/result.js';
|
|
6
10
|
/**
|
|
7
11
|
* Read the hook payload. `PEAKS_HOOK_STDIN` is a test seam (same convention as
|
|
@@ -77,6 +81,21 @@ export function registerHookHandleCommand(program, io) {
|
|
|
77
81
|
// Dispatch by toolName. For slice #1+, we only handle Bash. Task tool sub-agent dispatch goes through `peaks sub-agent dispatch` (slice #009) and does not need a hook entry.
|
|
78
82
|
// Other tools: allow (no-op; future events will be added here).
|
|
79
83
|
if (hook.toolName === 'Bash' && typeof fallbackCommand === 'string' && fallbackCommand.trim().length > 0) {
|
|
84
|
+
// L2.1 P0 #1: solo-code-ban. Deny `git commit` / `git apply` from peaks-* skills
|
|
85
|
+
// BEFORE the SOP gate runs. The active skill is read from the per-caller
|
|
86
|
+
// active-skill file (see active-skill-resolver.ts).
|
|
87
|
+
const activeSkill = resolveActiveSkillForCaller(projectRoot);
|
|
88
|
+
if (activeSkill.skill !== null) {
|
|
89
|
+
const soloDecision = evaluateSoloCodeBan({ skill: activeSkill.skill, command: fallbackCommand });
|
|
90
|
+
if (soloDecision.denied) {
|
|
91
|
+
const formatted = formatDecisionResponse(ide, 'deny', soloDecision.reason);
|
|
92
|
+
io.stdout(formatted.stdout);
|
|
93
|
+
if (options.json === true) {
|
|
94
|
+
io.stderr(JSON.stringify(ok('hook.handle', { ide, tool: hook.toolName, decision: 'deny', reason: soloDecision.reason, enforcer: 'solo-code-ban' })));
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
80
99
|
// Lazy import to avoid circular: peaks gate enforce logic
|
|
81
100
|
const { enforceBashCommand } = await import('../../services/sop/gate-enforce-service.js');
|
|
82
101
|
const decision = await enforceBashCommand(projectRoot, fallbackCommand);
|
|
@@ -89,6 +108,37 @@ export function registerHookHandleCommand(program, io) {
|
|
|
89
108
|
return;
|
|
90
109
|
}
|
|
91
110
|
}
|
|
111
|
+
// L2.2 P1 #1: login-gate. After solo-code-ban + gate-enforce pass,
|
|
112
|
+
// flag destructive patterns (uninstall, force-push, --force, --hard,
|
|
113
|
+
// rm -rf) so the LLM gets a soft warning (still allow). The user
|
|
114
|
+
// gets the warning via the warn channel; the command proceeds.
|
|
115
|
+
if (hook.toolName === 'Bash' && typeof fallbackCommand === 'string') {
|
|
116
|
+
const gate = checkLoginGate({ command: fallbackCommand });
|
|
117
|
+
if (gate.destructive) {
|
|
118
|
+
io.stderr(`warning: login-gate: destructive command detected (pattern: ${gate.matchedPattern}). Confirm with the user before proceeding.`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// L2.1 P0 #2: no-root-pollution. Deny Write/Edit to files outside the
|
|
122
|
+
// root allowlist. file_path is read from toolInput.file_path (Claude
|
|
123
|
+
// and most IDEs use the same shape). When the field is missing or the
|
|
124
|
+
// path is not at depth 1, the enforcer allows (no-op).
|
|
125
|
+
if (hook.toolName === 'Write' || hook.toolName === 'Edit' || hook.toolName === 'MultiEdit' || hook.toolName === 'Create') {
|
|
126
|
+
const filePath = pluckString(parsed, ['tool_input', 'file_path'])
|
|
127
|
+
?? pluckString(parsed, ['toolInput', 'file_path'])
|
|
128
|
+
?? pluckString(parsed, ['tool_input', 'path'])
|
|
129
|
+
?? pluckString(parsed, ['toolInput', 'path']);
|
|
130
|
+
if (typeof filePath === 'string' && filePath.trim().length > 0) {
|
|
131
|
+
const rootCheck = isRootWrite({ projectRoot, filePath });
|
|
132
|
+
if (!rootCheck.allowed) {
|
|
133
|
+
const formatted = formatDecisionResponse(ide, 'deny', rootCheck.denyReason);
|
|
134
|
+
io.stdout(formatted.stdout);
|
|
135
|
+
if (options.json === true) {
|
|
136
|
+
io.stderr(JSON.stringify(ok('hook.handle', { ide, tool: hook.toolName, decision: 'deny', reason: rootCheck.denyReason, enforcer: 'no-root-pollution' })));
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
92
142
|
const allow = formatDecisionResponse(ide, 'allow');
|
|
93
143
|
io.stdout(allow.stdout);
|
|
94
144
|
if (options.json === true) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peaks loop * CLI (Slice #14) — L4 Agent Loop Integration.
|
|
3
|
+
*
|
|
4
|
+
* Per docs/superpowers/specs/2026-06-11-peaks-cli-l1-l2-l3-redesign.md §5.4
|
|
5
|
+
* Slice #14, ships 5 sub-features:
|
|
6
|
+
*
|
|
7
|
+
* 14.1 peaks loop distill — extract patterns from past sessions
|
|
8
|
+
* (delegates to peaks memory extract)
|
|
9
|
+
* 14.2 peaks loop preflight — pre-run sanity checks (placeholder)
|
|
10
|
+
* 14.3 peaks loop detect-pattern — find repeating patterns (placeholder)
|
|
11
|
+
* 14.4 peaks loop check-consistency — verify state consistency (placeholder)
|
|
12
|
+
* 14.5 peaks goal compose — autonomous goal composition (placeholder;
|
|
13
|
+
* requires IDE adapter goalCommand capability
|
|
14
|
+
* per Slice #0.7)
|
|
15
|
+
*
|
|
16
|
+
* The 4 placeholders emit a clear nextActions list; the LLM-side UX
|
|
17
|
+
* layer composes the actual runtime today.
|
|
18
|
+
*/
|
|
19
|
+
import { Command } from 'commander';
|
|
20
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
21
|
+
export declare function registerLoopCommands(program: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peaks loop * CLI (Slice #14) — L4 Agent Loop Integration.
|
|
3
|
+
*
|
|
4
|
+
* Per docs/superpowers/specs/2026-06-11-peaks-cli-l1-l2-l3-redesign.md §5.4
|
|
5
|
+
* Slice #14, ships 5 sub-features:
|
|
6
|
+
*
|
|
7
|
+
* 14.1 peaks loop distill — extract patterns from past sessions
|
|
8
|
+
* (delegates to peaks memory extract)
|
|
9
|
+
* 14.2 peaks loop preflight — pre-run sanity checks (placeholder)
|
|
10
|
+
* 14.3 peaks loop detect-pattern — find repeating patterns (placeholder)
|
|
11
|
+
* 14.4 peaks loop check-consistency — verify state consistency (placeholder)
|
|
12
|
+
* 14.5 peaks goal compose — autonomous goal composition (placeholder;
|
|
13
|
+
* requires IDE adapter goalCommand capability
|
|
14
|
+
* per Slice #0.7)
|
|
15
|
+
*
|
|
16
|
+
* The 4 placeholders emit a clear nextActions list; the LLM-side UX
|
|
17
|
+
* layer composes the actual runtime today.
|
|
18
|
+
*/
|
|
19
|
+
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
20
|
+
import { fail, ok } from '../../shared/result.js';
|
|
21
|
+
export function registerLoopCommands(program, io) {
|
|
22
|
+
// 14.5 peaks goal compose — registered as a TOP-LEVEL command (not under
|
|
23
|
+
// `peaks loop`) because IDE adapters expose it as `goalCommand`.
|
|
24
|
+
// The sub-agent dispatch path consumes it; the slice 0.7 hermes +
|
|
25
|
+
// openclaw adapters will thread it through.
|
|
26
|
+
addJsonOption(program
|
|
27
|
+
.command('goal')
|
|
28
|
+
.description('14.5: compose an autonomous goal (returns the goal envelope that the LLM-side UX layer feeds to peaks sub-agent dispatch)')
|
|
29
|
+
.requiredOption('--project <path>', 'target project root')
|
|
30
|
+
.requiredOption('--goal <text>', 'the high-level goal to compose')).action(async (options) => {
|
|
31
|
+
try {
|
|
32
|
+
printResult(io, ok('goal.compose', {
|
|
33
|
+
project: options.project,
|
|
34
|
+
goal: options.goal,
|
|
35
|
+
status: 'placeholder',
|
|
36
|
+
nextSteps: [
|
|
37
|
+
'The composed goal is consumed by peaks sub-agent dispatch.',
|
|
38
|
+
'The hermes + openclaw IDE adapters (Slice #0.7) surface this as a goalCommand.',
|
|
39
|
+
],
|
|
40
|
+
}, [], [
|
|
41
|
+
'goal.compose is a thin facade; the LLM-side UX layer decomposes the goal into sub-agent tasks.',
|
|
42
|
+
]), options.json);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
printResult(io, fail('goal.compose', 'GOAL_COMPOSE_FAILED', getErrorMessage(error), { project: options.project, goal: options.goal }, ['Verify the project path and --goal value']), options.json);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
// peaks loop *
|
|
50
|
+
const loop = program.command('loop').description('Slice #14: L4 Agent Loop sub-features (distill / preflight / detect-pattern / check-consistency)');
|
|
51
|
+
// 14.1 distill
|
|
52
|
+
addJsonOption(loop.command('distill')
|
|
53
|
+
.description('14.1: distill patterns from past sessions into .peaks/memory/ (delegates to peaks memory extract)')
|
|
54
|
+
.requiredOption('--project <path>', 'target project root')
|
|
55
|
+
.option('--apply', 'write extracted memories to .peaks/memory/ (default: dry-run preview)', false)).action(async (options) => {
|
|
56
|
+
try {
|
|
57
|
+
const apply = options.apply === true;
|
|
58
|
+
// Delegate to the existing peaks memory extract CLI via dynamic
|
|
59
|
+
// import (avoids circular); the LLM-side UX layer composes the
|
|
60
|
+
// two commands.
|
|
61
|
+
const { execFileSync } = await import('node:child_process');
|
|
62
|
+
const args = ['memory', 'extract', '--project', options.project];
|
|
63
|
+
if (apply)
|
|
64
|
+
args.push('--apply');
|
|
65
|
+
const stdout = execFileSync('node', ['bin/peaks.js', ...args], {
|
|
66
|
+
cwd: options.project,
|
|
67
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
68
|
+
}).toString('utf-8');
|
|
69
|
+
printResult(io, ok('loop.distill', {
|
|
70
|
+
project: options.project,
|
|
71
|
+
apply,
|
|
72
|
+
delegateStdout: stdout.slice(0, 200),
|
|
73
|
+
}, [], [
|
|
74
|
+
apply ? 'peaks memory extract --apply was invoked' : 'peaks memory extract dry-run was invoked',
|
|
75
|
+
'A future slice will inline the memory extract (not via execFileSync).',
|
|
76
|
+
]), options.json);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
printResult(io, fail('loop.distill', 'LOOP_DISTILL_FAILED', getErrorMessage(error), { project: options.project }, ['Verify the project path']), options.json);
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
// 14.2 preflight
|
|
84
|
+
addJsonOption(loop.command('preflight')
|
|
85
|
+
.description('14.2: pre-run sanity checks (placeholder; future slice runs peaks doctor + peaks audit before each loop iter)')
|
|
86
|
+
.requiredOption('--project <path>', 'target project root')).action(async (options) => {
|
|
87
|
+
printResult(io, ok('loop.preflight', {
|
|
88
|
+
project: options.project,
|
|
89
|
+
status: 'placeholder',
|
|
90
|
+
nextSteps: [
|
|
91
|
+
'For each L4 loop iteration, call peaks doctor + peaks audit to surface regressions.',
|
|
92
|
+
'A future slice will inline the preflight checks (not just placeholder).',
|
|
93
|
+
],
|
|
94
|
+
}, [], [
|
|
95
|
+
'loop.preflight is a thin facade; the LLM-side UX layer composes peaks doctor + peaks audit.',
|
|
96
|
+
]), options.json);
|
|
97
|
+
});
|
|
98
|
+
// 14.3 detect-pattern
|
|
99
|
+
addJsonOption(loop.command('detect-pattern')
|
|
100
|
+
.description('14.3: detect repeating patterns across past sessions (placeholder; future slice uses peaks retrospective search)')
|
|
101
|
+
.requiredOption('--project <path>', 'target project root')).action(async (options) => {
|
|
102
|
+
printResult(io, ok('loop.detect-pattern', {
|
|
103
|
+
project: options.project,
|
|
104
|
+
status: 'placeholder',
|
|
105
|
+
nextSteps: [
|
|
106
|
+
'Run peaks retrospective search --limit 50 to surface high-frequency patterns.',
|
|
107
|
+
'A future slice will rank by frequency + LLM confidence.',
|
|
108
|
+
],
|
|
109
|
+
}, [], [
|
|
110
|
+
'loop.detect-pattern is a thin facade; the LLM-side UX layer composes peaks retrospective search.',
|
|
111
|
+
]), options.json);
|
|
112
|
+
});
|
|
113
|
+
// 14.4 check-consistency
|
|
114
|
+
addJsonOption(loop.command('check-consistency')
|
|
115
|
+
.description('14.4: verify state consistency (placeholder; future slice compares .peaks/_runtime across sessions)')
|
|
116
|
+
.requiredOption('--project <path>', 'target project root')).action(async (options) => {
|
|
117
|
+
printResult(io, ok('loop.check-consistency', {
|
|
118
|
+
project: options.project,
|
|
119
|
+
status: 'placeholder',
|
|
120
|
+
nextSteps: [
|
|
121
|
+
'Compare .peaks/_runtime/<sid>/session.json across recent sessions for drift.',
|
|
122
|
+
'A future slice will report drift with severity (warn / fail).',
|
|
123
|
+
],
|
|
124
|
+
}, [], [
|
|
125
|
+
'loop.check-consistency is a thin facade; the LLM-side UX layer composes the drift scan.',
|
|
126
|
+
]), options.json);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -5,6 +5,8 @@ import { renderOpenSpecChange } from '../../services/openspec/openspec-render-se
|
|
|
5
5
|
import { validateOpenSpecChange } from '../../services/openspec/openspec-validate-service.js';
|
|
6
6
|
import { archiveOpenSpecChange } from '../../services/openspec/openspec-archive-service.js';
|
|
7
7
|
import { executeOpenSpecInit } from '../../services/openspec/openspec-init-service.js';
|
|
8
|
+
import { proposeFromDoctor } from '../../services/openspec/openspec-propose-from-doctor-service.js';
|
|
9
|
+
import { runDoctor } from '../../services/doctor/doctor-service.js';
|
|
8
10
|
import { fail, ok } from '../../shared/result.js';
|
|
9
11
|
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
10
12
|
function resolveScanOptions(project) {
|
|
@@ -197,4 +199,39 @@ export function registerOpenSpecCommands(program, io) {
|
|
|
197
199
|
process.exitCode = 1;
|
|
198
200
|
}
|
|
199
201
|
});
|
|
202
|
+
addJsonOption(openspec
|
|
203
|
+
.command('from-doctor')
|
|
204
|
+
.description('Slice L3.3: generate an OpenSpec change draft (proposal.md) from a peaks doctor finding')
|
|
205
|
+
.requiredOption('--project <path>', 'target project root')
|
|
206
|
+
.requiredOption('--check-id <id>', 'the doctor check id to draft from (e.g. L3:l3-memory-health)')).action(async (options) => {
|
|
207
|
+
try {
|
|
208
|
+
const report = await runDoctor();
|
|
209
|
+
const finding = report.checks.find((c) => c.id === options.checkId);
|
|
210
|
+
if (finding === undefined) {
|
|
211
|
+
printResult(io, fail('openspec.from-doctor', 'CHECK_NOT_FOUND', `No doctor check with id "${options.checkId}"`, { availableIds: report.checks.map((c) => c.id) }, ['Run peaks doctor --json to list available check ids']), options.json);
|
|
212
|
+
process.exitCode = 1;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (finding.ok) {
|
|
216
|
+
printResult(io, fail('openspec.from-doctor', 'CHECK_ALREADY_PASSING', `Doctor check "${options.checkId}" is already passing; nothing to draft`, { checkId: options.checkId }, ['Pick a failing check from peaks doctor --json']), options.json);
|
|
217
|
+
process.exitCode = 1;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const doctorFinding = {
|
|
221
|
+
id: finding.id,
|
|
222
|
+
rule: finding.rule ?? finding.id,
|
|
223
|
+
detail: finding.message,
|
|
224
|
+
severity: 'fail',
|
|
225
|
+
};
|
|
226
|
+
const result = proposeFromDoctor({ projectRoot: options.project, finding: doctorFinding });
|
|
227
|
+
printResult(io, ok('openspec.from-doctor', result, [], [
|
|
228
|
+
`draft proposal written to ${result.proposalPath}`,
|
|
229
|
+
'Review + edit the draft, then run `peaks openspec validate <id>`',
|
|
230
|
+
]), options.json);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
printResult(io, fail('openspec.from-doctor', 'OPENSPEC_FROM_DOCTOR_FAILED', getErrorMessage(error), { projectRoot: options.project, checkId: options.checkId }, ['Verify the project path and the check id']), options.json);
|
|
234
|
+
process.exitCode = 1;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
200
237
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { loadPreferences, preferencesPath, savePreferences, } from '../../services/preferences/preferences-service.js';
|
|
4
|
+
const ALLOWED_KEYS = new Set([
|
|
5
|
+
'economyMode',
|
|
6
|
+
'swarmMode',
|
|
7
|
+
'uaPrompt',
|
|
8
|
+
'agentShieldPrompt',
|
|
9
|
+
'classifyConservatism',
|
|
10
|
+
'classifyRules',
|
|
11
|
+
'headroom',
|
|
12
|
+
'swarmSpeculative',
|
|
13
|
+
'loopAutonomousEnabled',
|
|
14
|
+
]);
|
|
15
|
+
export function registerPreferencesCommands(program) {
|
|
16
|
+
const prefs = program
|
|
17
|
+
.command('preferences')
|
|
18
|
+
.description('Manage project-local preferences (`.peaks/preferences.json`)');
|
|
19
|
+
prefs
|
|
20
|
+
.command('get')
|
|
21
|
+
.description('Get a preference value (from override or default)')
|
|
22
|
+
.requiredOption('--key <key>', 'preference key')
|
|
23
|
+
.option('--project <path>', 'project root', process.cwd())
|
|
24
|
+
.option('--json', 'JSON envelope output')
|
|
25
|
+
.action((opts) => {
|
|
26
|
+
try {
|
|
27
|
+
if (!ALLOWED_KEYS.has(opts.key)) {
|
|
28
|
+
throw new Error(`PREFERENCES_KEY_UNKNOWN: ${opts.key}`);
|
|
29
|
+
}
|
|
30
|
+
const all = loadPreferences(opts.project);
|
|
31
|
+
const value = all[opts.key];
|
|
32
|
+
const filePath = preferencesPath(opts.project);
|
|
33
|
+
// Spec bug fix: use the top-level existsSync import (the spec's
|
|
34
|
+
// `await import('node:fs')` would not compile inside a non-async action).
|
|
35
|
+
// Source is 'override' only when the key is explicitly present in
|
|
36
|
+
// the on-disk file (not merely when the file exists, since reset()
|
|
37
|
+
// may leave the file with other keys still overridden).
|
|
38
|
+
let source = 'default';
|
|
39
|
+
if (existsSync(filePath)) {
|
|
40
|
+
try {
|
|
41
|
+
const raw = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
42
|
+
if (Object.prototype.hasOwnProperty.call(raw, opts.key)) {
|
|
43
|
+
source = 'override';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Corrupt file: report the merged value but fall back to 'default'
|
|
48
|
+
// source to avoid claiming an override that cannot be parsed.
|
|
49
|
+
source = 'default';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Stable ordering: source is computed, value reflects merged state.
|
|
53
|
+
const envelope = {
|
|
54
|
+
ok: true,
|
|
55
|
+
data: { key: opts.key, value, source },
|
|
56
|
+
};
|
|
57
|
+
process.stdout.write(JSON.stringify(envelope, null, 2) + '\n');
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
process.stderr.write(err.message + '\n');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
prefs
|
|
65
|
+
.command('set')
|
|
66
|
+
.description('Override a preference value (writes to .peaks/preferences.json)')
|
|
67
|
+
.requiredOption('--key <key>', 'preference key')
|
|
68
|
+
.requiredOption('--value <value>', 'value (parsed as JSON, or string if not JSON)')
|
|
69
|
+
.option('--project <path>', 'project root', process.cwd())
|
|
70
|
+
.option('--json', 'JSON envelope output')
|
|
71
|
+
.action((opts) => {
|
|
72
|
+
try {
|
|
73
|
+
if (!ALLOWED_KEYS.has(opts.key)) {
|
|
74
|
+
throw new Error(`PREFERENCES_KEY_UNKNOWN: ${opts.key}`);
|
|
75
|
+
}
|
|
76
|
+
let parsed = opts.value;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(opts.value);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Not valid JSON — keep the raw string.
|
|
82
|
+
}
|
|
83
|
+
const merged = savePreferences(opts.project, {
|
|
84
|
+
[opts.key]: parsed,
|
|
85
|
+
});
|
|
86
|
+
const envelope = {
|
|
87
|
+
ok: true,
|
|
88
|
+
data: {
|
|
89
|
+
key: opts.key,
|
|
90
|
+
value: merged[opts.key],
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
process.stdout.write(JSON.stringify(envelope, null, 2) + '\n');
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
process.stderr.write(err.message + '\n');
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
prefs
|
|
101
|
+
.command('reset')
|
|
102
|
+
.description('Remove the override for a key (falls back to default)')
|
|
103
|
+
.requiredOption('--key <key>', 'preference key')
|
|
104
|
+
.option('--project <path>', 'project root', process.cwd())
|
|
105
|
+
.option('--json', 'JSON envelope output')
|
|
106
|
+
.action((opts) => {
|
|
107
|
+
try {
|
|
108
|
+
if (!ALLOWED_KEYS.has(opts.key)) {
|
|
109
|
+
throw new Error(`PREFERENCES_KEY_UNKNOWN: ${opts.key}`);
|
|
110
|
+
}
|
|
111
|
+
const filePath = preferencesPath(opts.project);
|
|
112
|
+
// Read raw on-disk state (not the merged view) so removing a key
|
|
113
|
+
// truly removes the override, instead of savePreferences re-inserting
|
|
114
|
+
// the default value. If no file exists, there is nothing to reset.
|
|
115
|
+
if (!existsSync(filePath)) {
|
|
116
|
+
const envelope = {
|
|
117
|
+
ok: true,
|
|
118
|
+
data: { key: opts.key, removed: false, reason: 'no-override-file' },
|
|
119
|
+
};
|
|
120
|
+
process.stdout.write(JSON.stringify(envelope) + '\n');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
let raw;
|
|
124
|
+
try {
|
|
125
|
+
raw = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
throw new Error(`PREFERENCES_JSON_INVALID: failed to parse ${filePath}: ${err.message}`);
|
|
129
|
+
}
|
|
130
|
+
const hadKey = Object.prototype.hasOwnProperty.call(raw, opts.key);
|
|
131
|
+
if (hadKey) {
|
|
132
|
+
delete raw[opts.key];
|
|
133
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
134
|
+
writeFileSync(filePath, JSON.stringify(raw, null, 2) + '\n', 'utf8');
|
|
135
|
+
}
|
|
136
|
+
const envelope = {
|
|
137
|
+
ok: true,
|
|
138
|
+
data: { key: opts.key, removed: hadKey },
|
|
139
|
+
};
|
|
140
|
+
process.stdout.write(JSON.stringify(envelope) + '\n');
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
process.stderr.write(err.message + '\n');
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peaks skills audit-conformance CLI (Slice #12) — runs the
|
|
3
|
+
* skill-conformance-service against all 12 peaks-* SKILL.md files and
|
|
4
|
+
* reports the 5 standard checks (frontmatter, CLI-back, loadStrategy,
|
|
5
|
+
* 800-line cap, outputStyle).
|
|
6
|
+
*/
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
9
|
+
export declare function registerSkillConformanceCommands(program: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peaks skills audit-conformance CLI (Slice #12) — runs the
|
|
3
|
+
* skill-conformance-service against all 12 peaks-* SKILL.md files and
|
|
4
|
+
* reports the 5 standard checks (frontmatter, CLI-back, loadStrategy,
|
|
5
|
+
* 800-line cap, outputStyle).
|
|
6
|
+
*/
|
|
7
|
+
import { auditSkillConformance } from '../../services/skills/skill-conformance-service.js';
|
|
8
|
+
import { getErrorMessage, printResult } from '../cli-helpers.js';
|
|
9
|
+
import { fail, ok } from '../../shared/result.js';
|
|
10
|
+
export function registerSkillConformanceCommands(program, io) {
|
|
11
|
+
program
|
|
12
|
+
.command('skills:audit-conformance')
|
|
13
|
+
.description('Slice #12: audit all 12 peaks-* SKILL.md against the 5 alignment standards')
|
|
14
|
+
.requiredOption('--project <path>', 'target project root')
|
|
15
|
+
.option('--json', 'print machine-readable JSON envelope')
|
|
16
|
+
.action(async (options) => {
|
|
17
|
+
try {
|
|
18
|
+
const report = auditSkillConformance({ projectRoot: options.project });
|
|
19
|
+
const nextActions = [];
|
|
20
|
+
if (report.failed > 0) {
|
|
21
|
+
nextActions.push(`${report.failed} hard failure(s); fix before shipping.`);
|
|
22
|
+
for (const c of report.checks.filter((c) => c.level === 'fail')) {
|
|
23
|
+
nextActions.push(` - ${c.skill}: ${c.id} — ${c.message}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (report.warned > 0) {
|
|
27
|
+
nextActions.push(`${report.warned} advisory warning(s); see envelope.checks for details.`);
|
|
28
|
+
}
|
|
29
|
+
if (report.failed === 0 && report.warned === 0) {
|
|
30
|
+
nextActions.push('All 13 skills pass the 5 alignment standards.');
|
|
31
|
+
}
|
|
32
|
+
printResult(io, ok('skills.audit-conformance', report, [], nextActions), options.json);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
printResult(io, fail('skills.audit-conformance', 'AUDIT_CONFORMANCE_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Verify the project path']), options.json);
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -75,4 +75,38 @@ export function registerUnderstandCommands(program, io) {
|
|
|
75
75
|
process.exitCode = 1;
|
|
76
76
|
}
|
|
77
77
|
});
|
|
78
|
+
// L3.1: opt-in UX subcommand. Returns the AskUserQuestion payload that
|
|
79
|
+
// the LLM-side UX layer (peaks-solo / peaks-ide) should surface when
|
|
80
|
+
// uaPrompt === 'unset' and UA is absent. When uaPrompt is skip-this-session
|
|
81
|
+
// or skip-forever, returns a no-op envelope (caller does not prompt).
|
|
82
|
+
addJsonOption(understand
|
|
83
|
+
.command('opt-in')
|
|
84
|
+
.description('Returns the UA opt-in prompt payload (Slice L3.1) when uaPrompt is unset; no-op otherwise')
|
|
85
|
+
.requiredOption('--project <path>', 'target project root')).action(async (options) => {
|
|
86
|
+
try {
|
|
87
|
+
const report = await scanUnderstandAnything({ projectRoot: options.project });
|
|
88
|
+
const uaPrompt = report.uaPrompt ?? 'unset';
|
|
89
|
+
if (report.exists || uaPrompt !== 'unset') {
|
|
90
|
+
// No prompt needed: UA is installed OR user already decided.
|
|
91
|
+
printResult(io, ok('understand.opt-in', { promptNeeded: false, uaPrompt, uaInstalled: report.exists }), options.json);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const prompt = {
|
|
95
|
+
version: 1,
|
|
96
|
+
tool: 'ua-opt-in',
|
|
97
|
+
artifactDir: report.artifactDir,
|
|
98
|
+
reason: 'ua-artifact-missing',
|
|
99
|
+
options: [
|
|
100
|
+
{ id: 'install', label: 'Install UA in Claude Code', description: INSTALL_HINT },
|
|
101
|
+
{ id: 'fallback-this-session', label: 'Use codegraph fallback this session', description: 'Skip UA this run; do not write preferences.json' },
|
|
102
|
+
{ id: 'fallback-forever', label: 'Use codegraph fallback forever', description: 'Write preferences.json:uaPrompt=skip-forever; suppress future prompts' }
|
|
103
|
+
]
|
|
104
|
+
};
|
|
105
|
+
printResult(io, ok('understand.opt-in', { promptNeeded: true, uaPrompt, uaInstalled: false, prompt }), options.json);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
printResult(io, fail('understand.opt-in', 'UNDERSTAND_OPTIN_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check the project path']), options.json);
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
78
112
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peaks upgrade * CLI surface — Slice: 1.x → 2.0 umbrella +
|
|
3
|
+
* Slice 3: --detect-1x flag.
|
|
4
|
+
*
|
|
5
|
+
* Per the "one-key completion" + "minimal-user-operation" tenets
|
|
6
|
+
* (2026-06-11), the user's typical upgrade path is
|
|
7
|
+
* `npm i -g peaks-cli@2.0` (the postinstall does the upgrade).
|
|
8
|
+
*
|
|
9
|
+
* The `peaks upgrade --to 2.0` CLI is the manual fallback for
|
|
10
|
+
* when the postinstall is skipped (e.g. CI uses
|
|
11
|
+
* `--ignore-scripts`). The umbrella orchestrates 7 sub-commands:
|
|
12
|
+
* config-migrate / standards-migrate / memory-extract /
|
|
13
|
+
* hooks-install / skill-sync / audit-verify + write-upgrade-record.
|
|
14
|
+
*
|
|
15
|
+
* The `--detect-1x` flag (added in slice 3) is a read-only
|
|
16
|
+
* probe that the peaks-solo skill calls to gate the
|
|
17
|
+
* AskUserQuestion that prompts the 1.x → 2.0 upgrade. The
|
|
18
|
+
* probe returns the JSON envelope from the
|
|
19
|
+
* 1x-detector-service; it does NOT modify any files.
|
|
20
|
+
*/
|
|
21
|
+
import { Command } from 'commander';
|
|
22
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
23
|
+
export declare function registerUpgradeCommands(program: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { runUpgrade } from '../../services/upgrade/upgrade-service.js';
|
|
2
|
+
import { detect1xProjectState } from '../../services/upgrade/1x-detector-service.js';
|
|
3
|
+
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
4
|
+
import { fail, ok } from '../../shared/result.js';
|
|
5
|
+
export function registerUpgradeCommands(program, io) {
|
|
6
|
+
addJsonOption(program
|
|
7
|
+
.command('upgrade')
|
|
8
|
+
.description('Upgrade a peaks-cli 1.x project to 2.0. Umbrella that orquestrates 7 sub-commands: config-migrate / standards-migrate / memory-extract / hooks-install / skill-sync / audit-verify + write-upgrade-record. Per the "one-key completion" tenet, prefer letting `npm i -g peaks-cli@2.0` postinstall run this for you. Use `--detect-1x` for a read-only probe (no file writes) that the peaks-solo skill uses to gate the 1.x → 2.0 AskUserQuestion.')
|
|
9
|
+
.option('--to <version>', 'target version (only "2.0" supported)', '2.0')
|
|
10
|
+
.option('--project <path>', 'project root to upgrade (default: cwd)')
|
|
11
|
+
.option('--auto', 'non-interactive: accept soft-fail on any sub-step (used by the postinstall hook)')
|
|
12
|
+
.option('--detect-1x', 'read-only probe: returns the 1.x state as JSON (no file writes); consumed by peaks-solo Step 0.55 to gate the AskUserQuestion')).action((options) => {
|
|
13
|
+
const projectRoot = options.project ?? process.cwd();
|
|
14
|
+
// Branch 1: --detect-1x (read-only probe)
|
|
15
|
+
if (options.detect1x === true) {
|
|
16
|
+
try {
|
|
17
|
+
const state = detect1xProjectState(projectRoot);
|
|
18
|
+
const nextActions = [];
|
|
19
|
+
if (state.isOneX) {
|
|
20
|
+
nextActions.push(`Detected 1.x state. peaks-solo Step 0.55 should present an AskUserQuestion to invoke \`peaks upgrade --to 2.0 --auto --project ${state.projectRoot ?? projectRoot}\`.`);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
nextActions.push('No 1.x state detected. Proceed with the standing 2.0 layout.');
|
|
24
|
+
}
|
|
25
|
+
const envelope = ok('upgrade.detect-1x', state, [], nextActions);
|
|
26
|
+
printResult(io, envelope, options.json);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const message = getErrorMessage(error);
|
|
30
|
+
printResult(io, fail('upgrade.detect-1x', 'DETECT_1X_FAILED', message, { isOneX: false, signals: [], projectRoot: null, configPath: null }, [message]), options.json);
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Branch 2: the umbrella (existing behavior)
|
|
36
|
+
try {
|
|
37
|
+
const result = runUpgrade({ projectRoot, auto: options.auto === true });
|
|
38
|
+
const nextActions = [...result.nextActions];
|
|
39
|
+
if (result.failedCount > 0) {
|
|
40
|
+
nextActions.unshift(`${result.failedCount} sub-step(s) failed. Re-run \`peaks upgrade --to 2.0\` to retry.`);
|
|
41
|
+
}
|
|
42
|
+
if (result.upgradeRecordPath !== null) {
|
|
43
|
+
nextActions.push(`Upgrade record written: ${result.upgradeRecordPath}`);
|
|
44
|
+
}
|
|
45
|
+
const envelope = ok('upgrade', result, [...result.warnings], nextActions);
|
|
46
|
+
printResult(io, envelope, options.json);
|
|
47
|
+
if (result.failedCount > 0) {
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
const message = getErrorMessage(error);
|
|
53
|
+
printResult(io, fail('upgrade', 'UPGRADE_FAILED', message, { applied: false }, [message]), options.json);
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|