peaks-cli 1.4.2 → 2.0.0
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 +238 -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/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 +86 -0
- package/dist/src/cli/program.js +30 -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 +92 -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 +35 -2
- package/dist/src/services/config/config-service.js +81 -0
- package/dist/src/services/config/config-types.d.ts +58 -0
- package/dist/src/services/config/config-types.js +6 -0
- 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/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 +43 -0
- package/dist/src/services/skills/sync-service.js +99 -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/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-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 +10 -4
- 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
|
@@ -1,17 +1,55 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { isDirectory } from '../../shared/fs.js';
|
|
5
5
|
import { getCurrentChangeId } from '../../shared/change-id.js';
|
|
6
6
|
import { verifyPipeline } from '../workflow/pipeline-verify-service.js';
|
|
7
|
-
|
|
7
|
+
import { findMockViolations } from '../audit/enforcers/mock-placement.js';
|
|
8
|
+
import { runRedLinesAudit } from '../audit/red-lines-service.js';
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a CLI binary to a project-local path, falling back to
|
|
11
|
+
* the system `npx`. pnpm (and npm/yarn) all create
|
|
12
|
+
* `node_modules/.bin/<name>`:
|
|
13
|
+
*
|
|
14
|
+
* - On Unix, this is a symlink to the package's executable.
|
|
15
|
+
* - On Windows, this is a `.cmd` shim; `execFileSync` only
|
|
16
|
+
* resolves `.cmd` through the shell (PATHEXT), so we pass
|
|
17
|
+
* `shell: true` when invoking one. Without this, the
|
|
18
|
+
* Windows `npx ENOENT` false-positive from
|
|
19
|
+
* observations 2317 + 2792 reproduces for every local
|
|
20
|
+
* binary.
|
|
21
|
+
*
|
|
22
|
+
* Returns the command + args + a `shell` flag that the
|
|
23
|
+
* `runCommand` helper threads into `execFileSync`.
|
|
24
|
+
*/
|
|
25
|
+
function resolveLocalBinary(projectRoot, name) {
|
|
26
|
+
// pnpm creates `node_modules/.bin/<name>` (symlink on Unix,
|
|
27
|
+
// `.cmd` shim on Windows). We probe both shapes; the
|
|
28
|
+
// `process.platform === 'win32'` extension probe is the most
|
|
29
|
+
// portable approach.
|
|
30
|
+
const isWin = process.platform === 'win32';
|
|
31
|
+
const candidateNames = isWin ? [`${name}.cmd`, `${name}.ps1`, `${name}`] : [name];
|
|
32
|
+
for (const candidate of candidateNames) {
|
|
33
|
+
const cmdPath = join(projectRoot, 'node_modules', '.bin', candidate);
|
|
34
|
+
if (existsSync(cmdPath)) {
|
|
35
|
+
return { command: cmdPath, args: [], shell: isWin };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Fallback: system npx. On Windows this still has the ENOENT
|
|
39
|
+
// issue, but the fallback is at least informative when it
|
|
40
|
+
// fires (the user can see "npx not found" instead of a
|
|
41
|
+
// silent exit 1).
|
|
42
|
+
return { command: 'npx', args: [name], shell: false };
|
|
43
|
+
}
|
|
44
|
+
function runCommand(command, args, cwd, timeoutMs, shell = false) {
|
|
8
45
|
const start = Date.now();
|
|
9
46
|
try {
|
|
10
47
|
const stdout = execFileSync(command, args, {
|
|
11
48
|
cwd,
|
|
12
49
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
13
50
|
timeout: timeoutMs,
|
|
14
|
-
maxBuffer: 32 * 1024 * 1024
|
|
51
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
52
|
+
shell
|
|
15
53
|
}).toString('utf8');
|
|
16
54
|
return {
|
|
17
55
|
status: 'pass',
|
|
@@ -41,11 +79,17 @@ function tailLines(text, max) {
|
|
|
41
79
|
}
|
|
42
80
|
async function runTypecheck(projectRoot) {
|
|
43
81
|
const start = Date.now();
|
|
44
|
-
|
|
82
|
+
// Per Windows npx ENOENT (observations 2317+2792 from
|
|
83
|
+
// 2026-06-09), prefer the project-local `node_modules/.bin/tsc`
|
|
84
|
+
// (symlink on Unix, .cmd on Windows). The local binary is
|
|
85
|
+
// installed by pnpm at workspace-install time and avoids the
|
|
86
|
+
// npx PATH-lookup issue.
|
|
87
|
+
const tsc = resolveLocalBinary(projectRoot, 'tsc');
|
|
88
|
+
const result = runCommand(tsc.command, [...tsc.args, '--noEmit'], projectRoot, 180_000, tsc.shell);
|
|
45
89
|
const testFiles = result.stdout.match(/(tests?\/.*\.test\.ts)/g) ?? [];
|
|
46
90
|
return {
|
|
47
91
|
name: 'typecheck',
|
|
48
|
-
description:
|
|
92
|
+
description: `${tsc.command} --noEmit (no JS emit, type-only check)`,
|
|
49
93
|
status: result.status,
|
|
50
94
|
durationMs: result.durationMs,
|
|
51
95
|
detail: result.status === 'pass'
|
|
@@ -76,13 +120,17 @@ async function runUnitTests(projectRoot, runTests) {
|
|
|
76
120
|
// state. Opt-in to the full suite via `runTests: true` (CLI flag
|
|
77
121
|
// `--run-tests`). See `references/runbook.md` for the rationale and
|
|
78
122
|
// `tests/unit/slice-check-service.test.ts` for the regression net.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
123
|
+
// Per Windows npx ENOENT (observations 2317+2792), resolve
|
|
124
|
+
// the project-local vitest binary instead of shelling out
|
|
125
|
+
// through npx.
|
|
126
|
+
const vitest = resolveLocalBinary(projectRoot, 'vitest');
|
|
127
|
+
const vitestArgs = runTests
|
|
128
|
+
? ['run', '--reporter=default', '--coverage=false']
|
|
129
|
+
: ['run', '--changed', '--reporter=default', '--coverage=false'];
|
|
82
130
|
const description = runTests
|
|
83
|
-
?
|
|
84
|
-
:
|
|
85
|
-
const result = runCommand(
|
|
131
|
+
? `${vitest.command} run (full test suite, coverage off)`
|
|
132
|
+
: `${vitest.command} run --changed (tests for git-changed files only, coverage off)`;
|
|
133
|
+
const result = runCommand(vitest.command, [...vitest.args, ...vitestArgs], projectRoot, 600_000, vitest.shell);
|
|
86
134
|
const summary = parseVitestSummary(result.stdout, result.durationMs);
|
|
87
135
|
// Vitest doesn't always print the per-bucket counts cleanly; infer "passed"
|
|
88
136
|
// as total - failed - skipped when failed/skipped buckets are present.
|
|
@@ -225,7 +273,7 @@ export async function sliceCheck(options) {
|
|
|
225
273
|
if (options.skipTests) {
|
|
226
274
|
stages.push({
|
|
227
275
|
name: 'unit-tests',
|
|
228
|
-
description: '
|
|
276
|
+
description: 'vitest run (skipped per --skip-tests)',
|
|
229
277
|
status: 'skipped',
|
|
230
278
|
durationMs: 0,
|
|
231
279
|
detail: 'Skipped: --skip-tests was set. Use the peaks-solo-test skill to run the full suite manually.'
|
|
@@ -244,7 +292,7 @@ export async function sliceCheck(options) {
|
|
|
244
292
|
const failureCount = unitTests.data?.failed ?? 0;
|
|
245
293
|
stages.push({
|
|
246
294
|
name: 'unit-tests',
|
|
247
|
-
description: `
|
|
295
|
+
description: `vitest run ${options.runTests === true ? '' : '--changed '} (overridden via --allow-pre-existing-failures)`.trim(),
|
|
248
296
|
status: 'skipped',
|
|
249
297
|
durationMs: unitTests.durationMs,
|
|
250
298
|
detail: `pre-existing failures: ${failureCount} failing test(s) under coverage.exclude or unrelated to this slice; user opted in via --allow-pre-existing-failures. For the long-term fix, mark these tests .skip or move to coverage.exclude (see dogfood-2-f1-f4.md F17c).`,
|
|
@@ -261,6 +309,17 @@ export async function sliceCheck(options) {
|
|
|
261
309
|
stages.push(await runReviewFanout(options.projectRoot, rid, options.refreshFanout));
|
|
262
310
|
// Stage 4: gate verify-pipeline
|
|
263
311
|
stages.push(await runGateVerifyPipeline(options.projectRoot, rid, rid));
|
|
312
|
+
// Stage 5: mock-placement (L2.1 P0 #5) — refuse inline mock data in src/ or skills/.
|
|
313
|
+
// Lifts changed files via `git diff --name-only HEAD`; falls back to a
|
|
314
|
+
// warning when the diff is empty (e.g. a fresh tree). Lighter than the
|
|
315
|
+
// full `peaks scan diff-vs-scope` and keeps the slice check self-contained.
|
|
316
|
+
stages.push(await runMockPlacement(options.projectRoot));
|
|
317
|
+
// Stage 6 (Slice #7 L2.4 P2-b): audit-regression — assert
|
|
318
|
+
// catalog integrity (no orphan enforcers, no orphan catalog
|
|
319
|
+
// entries), catalog size lower bound, and runtime budget.
|
|
320
|
+
// The stage runs `peaks audit red-lines` in-process (no
|
|
321
|
+
// subprocess) and is gating: failure exits non-zero.
|
|
322
|
+
stages.push(await runAuditRegression(options.projectRoot));
|
|
264
323
|
const boundaryReady = stages.every((s) => s.status === 'pass' || s.status === 'skipped');
|
|
265
324
|
const nextActions = [];
|
|
266
325
|
if (!boundaryReady) {
|
|
@@ -283,3 +342,97 @@ export async function sliceCheck(options) {
|
|
|
283
342
|
nextActions
|
|
284
343
|
};
|
|
285
344
|
}
|
|
345
|
+
async function runAuditRegression(projectRoot) {
|
|
346
|
+
const start = Date.now();
|
|
347
|
+
try {
|
|
348
|
+
const result = runRedLinesAudit({ projectRoot });
|
|
349
|
+
const durationMs = Date.now() - start;
|
|
350
|
+
// Slice #7 L2.4 P2-b acceptance A3 + A4:
|
|
351
|
+
// - totalRedLines >= 60 (catalog grew to 66; pins the lower bound)
|
|
352
|
+
// - enforcerFindings has no rl-audit-no-orphan-enforcer / rl-audit-no-orphan-catalog hits
|
|
353
|
+
const issues = [];
|
|
354
|
+
if (result.audit.totalRedLines < 60) {
|
|
355
|
+
issues.push(`totalRedLines ${result.audit.totalRedLines} < 60`);
|
|
356
|
+
}
|
|
357
|
+
const orphanFindings = result.audit.enforcerFindings.filter((f) => f.enforcerId === 'rl-audit-no-orphan-enforcer-001' ||
|
|
358
|
+
f.enforcerId === 'rl-audit-no-orphan-catalog-001');
|
|
359
|
+
if (orphanFindings.length > 0) {
|
|
360
|
+
issues.push(`${orphanFindings.length} orphan-enforcer / orphan-catalog finding(s)`);
|
|
361
|
+
}
|
|
362
|
+
if (issues.length > 0) {
|
|
363
|
+
return {
|
|
364
|
+
name: 'audit-regression',
|
|
365
|
+
description: 'audit-regression: catalog integrity + runtime budget (L2.4 P2-b stage 6)',
|
|
366
|
+
status: 'fail',
|
|
367
|
+
durationMs,
|
|
368
|
+
detail: issues.join('; '),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
name: 'audit-regression',
|
|
373
|
+
description: 'audit-regression: catalog integrity + runtime budget (L2.4 P2-b stage 6)',
|
|
374
|
+
status: 'pass',
|
|
375
|
+
durationMs,
|
|
376
|
+
detail: `catalog: ${result.audit.totalRedLines} entries (${result.audit.cliBacked} cli-backed, ${result.audit.proseOnly} prose-only); audit ran in ${durationMs}ms`,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
return {
|
|
381
|
+
name: 'audit-regression',
|
|
382
|
+
description: 'audit-regression: catalog integrity + runtime budget (L2.4 P2-b stage 6)',
|
|
383
|
+
status: 'fail',
|
|
384
|
+
durationMs: Date.now() - start,
|
|
385
|
+
detail: 'audit-regression failed: ' + (error?.message ?? String(error)),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async function runMockPlacement(projectRoot) {
|
|
390
|
+
const start = Date.now();
|
|
391
|
+
// List changed files via git. `--name-only` produces one path per line;
|
|
392
|
+
// we filter to text files in scope and read each.
|
|
393
|
+
const diffResult = runCommand('git', ['diff', '--name-only', '--diff-filter=ACMR', 'HEAD'], projectRoot, 30_000);
|
|
394
|
+
if (diffResult.status !== 'pass') {
|
|
395
|
+
return {
|
|
396
|
+
name: 'mock-placement',
|
|
397
|
+
description: 'mock-placement: no inline mock data in src/ or skills/ (L2.1 P0 #5)',
|
|
398
|
+
status: 'skipped',
|
|
399
|
+
durationMs: Date.now() - start,
|
|
400
|
+
detail: 'git diff failed or returned no changed files; mock-placement scan skipped.'
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
const changed = diffResult.stdout
|
|
404
|
+
.split('\n')
|
|
405
|
+
.map((l) => l.trim())
|
|
406
|
+
.filter(Boolean);
|
|
407
|
+
if (changed.length === 0) {
|
|
408
|
+
return {
|
|
409
|
+
name: 'mock-placement',
|
|
410
|
+
description: 'mock-placement: no inline mock data in src/ or skills/ (L2.1 P0 #5)',
|
|
411
|
+
status: 'skipped',
|
|
412
|
+
durationMs: Date.now() - start,
|
|
413
|
+
detail: 'no changed files in HEAD diff; mock-placement scan skipped.'
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const files = changed
|
|
417
|
+
.filter((p) => p.startsWith('src/') || p.startsWith('skills/'))
|
|
418
|
+
.filter((p) => p.endsWith('.ts') || p.endsWith('.tsx') || p.endsWith('.js') || p.endsWith('.mjs'))
|
|
419
|
+
.map((filePath) => {
|
|
420
|
+
const abs = join(projectRoot, filePath);
|
|
421
|
+
if (!existsSync(abs))
|
|
422
|
+
return null;
|
|
423
|
+
const content = readFileSync(abs, 'utf-8');
|
|
424
|
+
return { filePath, content };
|
|
425
|
+
})
|
|
426
|
+
.filter((f) => f !== null);
|
|
427
|
+
const violations = findMockViolations(files);
|
|
428
|
+
return {
|
|
429
|
+
name: 'mock-placement',
|
|
430
|
+
description: 'mock-placement: no inline mock data in src/ or skills/ (L2.1 P0 #5)',
|
|
431
|
+
status: violations.length === 0 ? 'pass' : 'fail',
|
|
432
|
+
durationMs: Date.now() - start,
|
|
433
|
+
detail: violations.length === 0
|
|
434
|
+
? `Scanned ${files.length} changed file(s); no inline mock data found.`
|
|
435
|
+
: `${violations.length} violation(s): ${violations.map((v) => `${v.filePath} (${v.snippet})`).join('; ')}`,
|
|
436
|
+
data: { scannedFiles: files.length, violations: violations.map((v) => ({ filePath: v.filePath, pattern: v.pattern, snippet: v.snippet })) }
|
|
437
|
+
};
|
|
438
|
+
}
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
export type SliceCheckStageStatus = 'pass' | 'fail' | 'skipped';
|
|
29
29
|
export type SliceCheckStage = {
|
|
30
30
|
/** Stable id for the stage (matches the runbook's check list). */
|
|
31
|
-
name: 'typecheck' | 'unit-tests' | 'review-fanout' | 'gate-verify-pipeline';
|
|
31
|
+
name: 'typecheck' | 'unit-tests' | 'review-fanout' | 'gate-verify-pipeline' | 'mock-placement' | 'audit-regression';
|
|
32
32
|
/** Human-readable description. */
|
|
33
33
|
description: string;
|
|
34
34
|
status: SliceCheckStageStatus;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface MigrateClaudeRulesInput {
|
|
2
|
+
readonly projectRoot: string;
|
|
3
|
+
readonly apply?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface MigrateClaudeRulesData {
|
|
6
|
+
readonly backupPath: string | null;
|
|
7
|
+
readonly thinnedFiles: readonly string[];
|
|
8
|
+
readonly scaffoldedFiles: readonly string[];
|
|
9
|
+
readonly preservedFiles: readonly string[];
|
|
10
|
+
readonly wouldChange: boolean;
|
|
11
|
+
readonly applied: boolean;
|
|
12
|
+
readonly nextActions: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
export interface MigrateClaudeRulesResult {
|
|
15
|
+
readonly ok: true;
|
|
16
|
+
readonly data: MigrateClaudeRulesData;
|
|
17
|
+
readonly warnings: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
export declare function migrateClaudeRules(input: MigrateClaudeRulesInput): MigrateClaudeRulesResult;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peaks standards migrate — .claude/rules/ tree thinning.
|
|
3
|
+
* Slice: 2026-06-12-standards-migrate-claude-rules.
|
|
4
|
+
*
|
|
5
|
+
* The 1.x peaks-cli install copied a thick .claude/rules
|
|
6
|
+
* tree (skill-first / CLI-auxiliary / dogfood / commit-trailer
|
|
7
|
+
* rules) into consumer projects. In 2.0, the canonical rules
|
|
8
|
+
* live at .peaks/standards/ and every markdown file under
|
|
9
|
+
* .claude/rules becomes a 2-line pointer to the canonical path.
|
|
10
|
+
*
|
|
11
|
+
* The service:
|
|
12
|
+
* 1. Backs up the existing `.claude/rules/` tree to
|
|
13
|
+
* `.claude/rules/.peaks-2.0-backup-<ts>/` (timestamped;
|
|
14
|
+
* safe to run multiple times).
|
|
15
|
+
* 2. Replaces each .md file under .claude/rules (recursive)
|
|
16
|
+
* with a 2-line pointer.
|
|
17
|
+
* 3. Scaffolds the 2.0 canonical rules at
|
|
18
|
+
* `.peaks/standards/{common,typescript}/`, but
|
|
19
|
+
* never overwrites existing files in `.peaks/standards/`.
|
|
20
|
+
*
|
|
21
|
+
* All operations are gated by `apply: true`. Dry-run mode
|
|
22
|
+
* returns the would-change diff without writing.
|
|
23
|
+
*/
|
|
24
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
const POINTER_TEXT = (canonicalPath) => `# Canonical peaks-cli 2.0 rules live at: ${canonicalPath}\n# This file is a 2-line pointer. Edit the canonical file instead.\n`;
|
|
27
|
+
function timestampSlug() {
|
|
28
|
+
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
29
|
+
}
|
|
30
|
+
function readMarkdownFilesRecursive(root) {
|
|
31
|
+
if (!existsSync(root))
|
|
32
|
+
return [];
|
|
33
|
+
const out = [];
|
|
34
|
+
const stat = statSync(root);
|
|
35
|
+
if (stat.isFile()) {
|
|
36
|
+
return root.endsWith('.md') ? [root] : [];
|
|
37
|
+
}
|
|
38
|
+
if (!stat.isDirectory())
|
|
39
|
+
return [];
|
|
40
|
+
for (const entry of readdirSync(root)) {
|
|
41
|
+
out.push(...readMarkdownFilesRecursive(join(root, entry)));
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
function isAlreadyPointer(filePath) {
|
|
46
|
+
if (!existsSync(filePath))
|
|
47
|
+
return false;
|
|
48
|
+
try {
|
|
49
|
+
const body = readFileSync(filePath, 'utf8');
|
|
50
|
+
return body.includes('Canonical peaks-cli 2.0 rules live at:');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const CANONICAL_2_0_DEV_PREFERENCE = `# Peaks-Cli dev preference (2.0 canonical)
|
|
57
|
+
|
|
58
|
+
> Project-local preference, captured from the 1.x install + re-rendered with the 2.0 vocabulary.
|
|
59
|
+
> Scope: applies to every iteration, adjustment, fix, or tweak on this project.
|
|
60
|
+
> Reading: read this **before** opening a new CLI command or routing a new feature through a CLI surface.
|
|
61
|
+
|
|
62
|
+
## Rule 1 — Skill-first, CLI-auxiliary
|
|
63
|
+
|
|
64
|
+
When designing or modifying a peaks-cli feature, default to the **skill-first** design. CLI commands are **invoked by the skill prompt** when they are the right primitive: a side effect that must be atomic, a gate that must be machine-enforced, a probe that needs structured JSON, or a backstop that prevents the LLM from skipping a step. Behaviour only an LLM in a skill prompt would use lives **in the relevant skill's SKILL.md**, not as a new CLI command. See \`.claude/rules/common/dev-preference.md\` for the decision template.
|
|
65
|
+
|
|
66
|
+
## Rule 2 — Dogfood on every adjustment
|
|
67
|
+
|
|
68
|
+
**Every adjustment, iteration, or fix-problem operation must be dogfood-tested in the current project before the work is declared complete.** No exceptions for "it's a small change", "just a comment update", or "just a SKILL.md line". The unit test suite is a subset of "current effect"; the dogfood is the full set. If a change passes unit tests but breaks a CLI command, the change is a regression.
|
|
69
|
+
|
|
70
|
+
## Rule 3 — Commits belong to the human
|
|
71
|
+
|
|
72
|
+
**No AI co-author trailer.** The commit is the human's. **Identity is global gitconfig only** (\`~/.gitconfig\`). Do not set, override, or shadow \`user.name\` / \`user.email\` at the repo level, via env vars, or via \`git -c user.*=...\`. The commit's recorded author and committer must both equal the global identity.
|
|
73
|
+
`;
|
|
74
|
+
const CANONICAL_2_0_CODING_STYLE_TS = `# TypeScript Coding Standards (2.0 canonical)
|
|
75
|
+
|
|
76
|
+
> Project-local standards, derived from the 1.x install + re-rendered with the 2.0 vocabulary.
|
|
77
|
+
|
|
78
|
+
- Apply project-local conventions before generic typescript guidance.
|
|
79
|
+
- Keep public APIs typed or documented according to typescript ecosystem norms.
|
|
80
|
+
- Do not add new \`any\` types; use explicit domain types, generics, or \`unknown\` with narrowing.
|
|
81
|
+
- Prefer standard tooling and existing project scripts for formatting, linting, tests, and coverage.
|
|
82
|
+
- peaks-rd must check this file before planning code changes in typescript projects.
|
|
83
|
+
`;
|
|
84
|
+
const CANONICAL_2_0_COMMON_FILES = [
|
|
85
|
+
{ relPath: 'common/dev-preference.md', content: CANONICAL_2_0_DEV_PREFERENCE },
|
|
86
|
+
{
|
|
87
|
+
relPath: 'common/coding-style.md',
|
|
88
|
+
content: '# Coding Standards (2.0 canonical)\n\n- Prefer simple, readable code over clever abstractions.\n- Keep functions focused and files cohesive.\n- Use immutable updates unless a language-specific convention explicitly favors mutation.\n- Validate user input, external data, file paths, and configuration at system boundaries.\n- Preserve existing project conventions when they are stricter than this baseline.\n',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
relPath: 'common/code-review.md',
|
|
92
|
+
content: '# Code Review Standards (2.0 canonical)\n\n- Review diffs for correctness, maintainability, test coverage, and regression risk.\n- Treat missing tests for changed behavior as a blocker unless the change is documentation-only.\n- Verify code paths that handle filesystem, external APIs, credentials, user input, or generated artifacts.\n',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
relPath: 'common/security.md',
|
|
96
|
+
content: '# Security Review Standards (2.0 canonical)\n\n- Never hardcode secrets, API keys, passwords, tokens, or credentials.\n- Do not send private code or secrets to external services without explicit user authorization.\n- Guard filesystem writes against path traversal, symlink, and junction escapes.\n- Require explicit confirmation for destructive actions, external state changes, and credential use.\n',
|
|
97
|
+
},
|
|
98
|
+
{ relPath: 'typescript/coding-style.md', content: CANONICAL_2_0_CODING_STYLE_TS },
|
|
99
|
+
];
|
|
100
|
+
export function migrateClaudeRules(input) {
|
|
101
|
+
const projectRoot = input.projectRoot;
|
|
102
|
+
const apply = input.apply === true;
|
|
103
|
+
const warnings = [];
|
|
104
|
+
const nextActions = [];
|
|
105
|
+
const claudeRulesDir = join(projectRoot, '.claude', 'rules');
|
|
106
|
+
const peaksStandardsDir = join(projectRoot, '.peaks', 'standards');
|
|
107
|
+
const canonicalRelPath = '.peaks/standards/';
|
|
108
|
+
const existingRulesFiles = readMarkdownFilesRecursive(claudeRulesDir);
|
|
109
|
+
const thickFiles = existingRulesFiles.filter((f) => !isAlreadyPointer(f));
|
|
110
|
+
const hasThickFiles = thickFiles.length > 0;
|
|
111
|
+
// The backup path is computed eagerly (so dry-run can preview
|
|
112
|
+
// the would-create location) but only created on disk in
|
|
113
|
+
// apply mode. In dry-run mode we still return the path so
|
|
114
|
+
// the user can see where the backup will land.
|
|
115
|
+
const computedBackupPath = hasThickFiles ? join(claudeRulesDir, `.peaks-2.0-backup-${timestampSlug()}`) : null;
|
|
116
|
+
const backupPath = apply ? computedBackupPath : null;
|
|
117
|
+
const thinnedFiles = [];
|
|
118
|
+
const scaffoldedFiles = [];
|
|
119
|
+
const preservedFiles = [];
|
|
120
|
+
// wouldChange is true iff there is at least one thick file to
|
|
121
|
+
// thin. An empty .claude/rules/ is NOT a wouldChange (no-op).
|
|
122
|
+
const wouldChange = hasThickFiles;
|
|
123
|
+
if (apply && hasThickFiles) {
|
|
124
|
+
// Step 1: backup
|
|
125
|
+
if (backupPath !== null) {
|
|
126
|
+
try {
|
|
127
|
+
mkdirSync(backupPath, { recursive: true });
|
|
128
|
+
for (const file of thickFiles) {
|
|
129
|
+
const body = readFileSync(file, 'utf8');
|
|
130
|
+
const rel = file.slice(claudeRulesDir.length + 1);
|
|
131
|
+
writeFileSync(join(backupPath, rel), body, 'utf8');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
warnings.push(`Backup step failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Step 2: replace each .md with a 2-line pointer
|
|
139
|
+
for (const file of thickFiles) {
|
|
140
|
+
try {
|
|
141
|
+
writeFileSync(file, POINTER_TEXT(canonicalRelPath), 'utf8');
|
|
142
|
+
thinnedFiles.push(file);
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
warnings.push(`Thin step failed for ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Step 3: scaffold .peaks/standards/ — never overwrite existing
|
|
149
|
+
for (const file of CANONICAL_2_0_COMMON_FILES) {
|
|
150
|
+
const dest = join(peaksStandardsDir, file.relPath);
|
|
151
|
+
if (existsSync(dest)) {
|
|
152
|
+
preservedFiles.push(dest);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
mkdirSync(join(dest, '..'), { recursive: true });
|
|
157
|
+
writeFileSync(dest, file.content, 'utf8');
|
|
158
|
+
scaffoldedFiles.push(dest);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
warnings.push(`Scaffold step failed for ${file.relPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (thinnedFiles.length > 0) {
|
|
166
|
+
nextActions.push(`Thinned ${thinnedFiles.length} .md file(s) under .claude/rules (recursive) → 2-line pointer.`);
|
|
167
|
+
}
|
|
168
|
+
if (scaffoldedFiles.length > 0) {
|
|
169
|
+
nextActions.push(`Scaffolded ${scaffoldedFiles.length} 2.0 canonical rule(s) at .peaks/standards/.`);
|
|
170
|
+
}
|
|
171
|
+
if (preservedFiles.length > 0) {
|
|
172
|
+
nextActions.push(`Preserved ${preservedFiles.length} existing .peaks/standards/ file(s) (no overwrite).`);
|
|
173
|
+
}
|
|
174
|
+
if (backupPath !== null) {
|
|
175
|
+
nextActions.push(`Backup at ${backupPath} (git-ignored).`);
|
|
176
|
+
}
|
|
177
|
+
if (!apply && wouldChange) {
|
|
178
|
+
nextActions.push('Re-run with --apply to perform the migration.');
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
ok: true,
|
|
182
|
+
data: {
|
|
183
|
+
backupPath,
|
|
184
|
+
thinnedFiles,
|
|
185
|
+
scaffoldedFiles,
|
|
186
|
+
preservedFiles,
|
|
187
|
+
wouldChange,
|
|
188
|
+
applied: apply && hasThickFiles,
|
|
189
|
+
nextActions,
|
|
190
|
+
},
|
|
191
|
+
warnings,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -2,6 +2,7 @@ import { stat } from 'node:fs/promises';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { isDirectory, pathExists, readText } from '../../shared/fs.js';
|
|
4
4
|
import { getErrorMessage } from '../../shared/result.js';
|
|
5
|
+
import { loadPreferences } from '../preferences/preferences-service.js';
|
|
5
6
|
function defaultArtifactDir(projectRoot) {
|
|
6
7
|
return join(projectRoot, '.understand-anything');
|
|
7
8
|
}
|
|
@@ -54,19 +55,31 @@ async function readGraph(graphPath) {
|
|
|
54
55
|
export async function scanUnderstandAnything(options) {
|
|
55
56
|
const artifactDir = options.artifactDir ?? defaultArtifactDir(options.projectRoot);
|
|
56
57
|
const exists = await isDirectory(artifactDir);
|
|
58
|
+
// L3.1: read uaPrompt from preferences.json (graceful — returns 'unset' if missing/broken)
|
|
59
|
+
const uaPrompt = await readUaPrompt(options.projectRoot);
|
|
57
60
|
if (!exists) {
|
|
58
61
|
return {
|
|
59
62
|
exists: false,
|
|
60
63
|
artifactDir,
|
|
61
64
|
graph: { exists: false, path: join(artifactDir, 'knowledge-graph.json') },
|
|
62
65
|
intermediate: { exists: false, path: join(artifactDir, 'intermediate') },
|
|
63
|
-
diffOverlay: { exists: false, path: join(artifactDir, 'diff-overlay.json') }
|
|
66
|
+
diffOverlay: { exists: false, path: join(artifactDir, 'diff-overlay.json') },
|
|
67
|
+
uaPrompt
|
|
64
68
|
};
|
|
65
69
|
}
|
|
66
70
|
const graph = await readGraph(join(artifactDir, 'knowledge-graph.json'));
|
|
67
71
|
const intermediate = await readFlag(join(artifactDir, 'intermediate'));
|
|
68
72
|
const diffOverlay = await readFlag(join(artifactDir, 'diff-overlay.json'));
|
|
69
|
-
return { exists: true, artifactDir, graph, intermediate, diffOverlay };
|
|
73
|
+
return { exists: true, artifactDir, graph, intermediate, diffOverlay, uaPrompt };
|
|
74
|
+
}
|
|
75
|
+
async function readUaPrompt(projectRoot) {
|
|
76
|
+
try {
|
|
77
|
+
const prefs = await loadPreferences(projectRoot);
|
|
78
|
+
return prefs.uaPrompt;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return 'unset';
|
|
82
|
+
}
|
|
70
83
|
}
|
|
71
84
|
function pickStringId(value) {
|
|
72
85
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
@@ -15,10 +15,36 @@ export type UnderstandFlagReport = {
|
|
|
15
15
|
exists: boolean;
|
|
16
16
|
path: string;
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* Slice L3.1 — UA opt-in UX state. 'unset' triggers an opt-in prompt on
|
|
20
|
+
* first scan; 'skip-this-session' suppresses the prompt for the current
|
|
21
|
+
* session; 'skip-forever' writes to .peaks/preferences.json to suppress
|
|
22
|
+
* all future prompts. Mirrors preferences.json:uaPrompt.
|
|
23
|
+
*/
|
|
24
|
+
export type UaPromptDecision = 'unset' | 'skip-this-session' | 'skip-forever';
|
|
18
25
|
export type UnderstandScanReport = {
|
|
19
26
|
exists: boolean;
|
|
20
27
|
artifactDir: string;
|
|
21
28
|
graph: UnderstandGraphReport;
|
|
22
29
|
intermediate: UnderstandFlagReport;
|
|
23
30
|
diffOverlay: UnderstandFlagReport;
|
|
31
|
+
/** Slice L3.1: opt-in UX state from preferences.json:uaPrompt. */
|
|
32
|
+
readonly uaPrompt?: UaPromptDecision;
|
|
24
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* Slice L3.1 — opt-in prompt payload. When uaPrompt === 'unset' and UA is
|
|
36
|
+
* absent, the peaks-solo / peaks-ide layer surfaces this to the user via
|
|
37
|
+
* AskUserQuestion. The CLI does not prompt directly; it returns this
|
|
38
|
+
* payload so the LLM-side UX layer can decide.
|
|
39
|
+
*/
|
|
40
|
+
export interface UaOptInPrompt {
|
|
41
|
+
readonly version: 1;
|
|
42
|
+
readonly tool: 'ua-opt-in';
|
|
43
|
+
readonly artifactDir: string;
|
|
44
|
+
readonly reason: 'ua-artifact-missing';
|
|
45
|
+
readonly options: readonly {
|
|
46
|
+
readonly id: 'install' | 'fallback-this-session' | 'fallback-forever';
|
|
47
|
+
readonly label: string;
|
|
48
|
+
readonly description: string;
|
|
49
|
+
}[];
|
|
50
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 1.x → 2.0 detection service — TypeScript mirror of
|
|
3
|
+
* `scripts/install-skills.mjs:detect1xProjectState`.
|
|
4
|
+
*
|
|
5
|
+
* Slice: 2026-06-12-solo-step-0-55-1x-detection.
|
|
6
|
+
*
|
|
7
|
+
* The canonical implementation lives in the .mjs postinstall
|
|
8
|
+
* (because the postinstall runs before any TS compile step).
|
|
9
|
+
* This TS mirror exists so the peaks-solo skill can call
|
|
10
|
+
* `peaks upgrade --detect-1x --project <root> --json` and
|
|
11
|
+
* read a structured JSON envelope to gate the
|
|
12
|
+
* AskUserQuestion that prompts the 1.x → 2.0 upgrade.
|
|
13
|
+
*
|
|
14
|
+
* The two implementations MUST stay in parity. The
|
|
15
|
+
* `tests/integration/upgrade/1x-detector-parity.test.ts`
|
|
16
|
+
* test exercises both on the same fixture and asserts
|
|
17
|
+
* their outputs match.
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
20
|
+
import { homedir } from 'node:os';
|
|
21
|
+
import { dirname, join } from 'node:path';
|
|
22
|
+
const MAX_WALK_UP = 8;
|
|
23
|
+
export function detect1xProjectState(cwd = process.cwd()) {
|
|
24
|
+
const home = homedir();
|
|
25
|
+
const signals = [];
|
|
26
|
+
let projectRoot = null;
|
|
27
|
+
let configPath = null;
|
|
28
|
+
// Walk up from cwd looking for .peaks/_runtime (signals
|
|
29
|
+
// we're inside a peaks project).
|
|
30
|
+
let dir = cwd;
|
|
31
|
+
for (let i = 0; i < MAX_WALK_UP; i += 1) {
|
|
32
|
+
const peaksRuntime = join(dir, '.peaks', '_runtime');
|
|
33
|
+
if (existsSync(peaksRuntime)) {
|
|
34
|
+
projectRoot = dir;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
const parent = dirname(dir);
|
|
38
|
+
if (parent === dir)
|
|
39
|
+
break;
|
|
40
|
+
dir = parent;
|
|
41
|
+
}
|
|
42
|
+
// Signal 1: ~/.peaks/config.json with 1.x version
|
|
43
|
+
const globalConfig = join(home, '.peaks', 'config.json');
|
|
44
|
+
if (existsSync(globalConfig)) {
|
|
45
|
+
try {
|
|
46
|
+
const raw = JSON.parse(readFileSync(globalConfig, 'utf8'));
|
|
47
|
+
if (typeof raw['version'] === 'string' && /^1\./.test(raw['version'])) {
|
|
48
|
+
signals.push(`global config at ${globalConfig} is 1.x (${raw['version']})`);
|
|
49
|
+
if (configPath === null)
|
|
50
|
+
configPath = globalConfig;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// ignore parse error — the 1.x detection is best-effort
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Signal 2: .claude/rules/common/dev-preference.md with peaks progress
|
|
58
|
+
if (projectRoot !== null) {
|
|
59
|
+
const devPref = join(projectRoot, '.claude', 'rules', 'common', 'dev-preference.md');
|
|
60
|
+
if (existsSync(devPref)) {
|
|
61
|
+
try {
|
|
62
|
+
const body = readFileSync(devPref, 'utf8');
|
|
63
|
+
if (/peaks progress/i.test(body)) {
|
|
64
|
+
signals.push(`${devPref} references "peaks progress" (1.x CLI surface, removed in slice #014)`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// ignore
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Signal 3: project preferences.json missing or 1.x
|
|
72
|
+
const prefs = join(projectRoot, '.peaks', 'preferences.json');
|
|
73
|
+
if (!existsSync(prefs)) {
|
|
74
|
+
signals.push(`${prefs} does not exist (1.x project never migrated)`);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
try {
|
|
78
|
+
const raw = JSON.parse(readFileSync(prefs, 'utf8'));
|
|
79
|
+
if (raw['schema_version'] !== '2.0.0') {
|
|
80
|
+
signals.push(`${prefs} has schema_version ${JSON.stringify(raw['schema_version'])}, expected '2.0.0'`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
signals.push(`${prefs} exists but is not valid JSON`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
isOneX: signals.length > 0,
|
|
90
|
+
signals,
|
|
91
|
+
projectRoot,
|
|
92
|
+
configPath,
|
|
93
|
+
};
|
|
94
|
+
}
|