peaks-cli 1.3.9 → 1.4.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.
Files changed (56) hide show
  1. package/dist/src/cli/commands/core-artifact-commands.js +27 -0
  2. package/dist/src/cli/commands/skill-scope-commands.d.ts +49 -0
  3. package/dist/src/cli/commands/skill-scope-commands.js +305 -0
  4. package/dist/src/cli/commands/workflow-commands.js +1 -1
  5. package/dist/src/cli/commands/workflow-plan-commands.d.ts +39 -0
  6. package/dist/src/cli/commands/workflow-plan-commands.js +163 -0
  7. package/dist/src/cli/program.js +6 -0
  8. package/dist/src/services/doctor/doctor-service.d.ts +40 -0
  9. package/dist/src/services/doctor/doctor-service.js +160 -0
  10. package/dist/src/services/hooks/presence-marker-detector.d.ts +16 -0
  11. package/dist/src/services/hooks/presence-marker-detector.js +105 -0
  12. package/dist/src/services/skill-scope/adapters/_stub-helper.d.ts +39 -0
  13. package/dist/src/services/skill-scope/adapters/_stub-helper.js +98 -0
  14. package/dist/src/services/skill-scope/adapters/claude-code.d.ts +59 -0
  15. package/dist/src/services/skill-scope/adapters/claude-code.js +304 -0
  16. package/dist/src/services/skill-scope/adapters/codex.d.ts +2 -0
  17. package/dist/src/services/skill-scope/adapters/codex.js +12 -0
  18. package/dist/src/services/skill-scope/adapters/cursor.d.ts +2 -0
  19. package/dist/src/services/skill-scope/adapters/cursor.js +13 -0
  20. package/dist/src/services/skill-scope/adapters/qoder.d.ts +2 -0
  21. package/dist/src/services/skill-scope/adapters/qoder.js +13 -0
  22. package/dist/src/services/skill-scope/adapters/tongyi.d.ts +2 -0
  23. package/dist/src/services/skill-scope/adapters/tongyi.js +13 -0
  24. package/dist/src/services/skill-scope/adapters/trae.d.ts +2 -0
  25. package/dist/src/services/skill-scope/adapters/trae.js +12 -0
  26. package/dist/src/services/skill-scope/detect.d.ts +75 -0
  27. package/dist/src/services/skill-scope/detect.js +480 -0
  28. package/dist/src/services/skill-scope/registry.d.ts +41 -0
  29. package/dist/src/services/skill-scope/registry.js +83 -0
  30. package/dist/src/services/skill-scope/source-of-truth.d.ts +44 -0
  31. package/dist/src/services/skill-scope/source-of-truth.js +118 -0
  32. package/dist/src/services/skill-scope/types.d.ts +176 -0
  33. package/dist/src/services/skill-scope/types.js +74 -0
  34. package/dist/src/services/standards/migrate-service.d.ts +63 -0
  35. package/dist/src/services/standards/migrate-service.js +193 -0
  36. package/dist/src/services/standards/project-standards-service.js +1 -23
  37. package/dist/src/services/workflow/artifact-paths.d.ts +59 -0
  38. package/dist/src/services/workflow/artifact-paths.js +127 -0
  39. package/dist/src/services/workflow/pipeline-verify-service.d.ts +6 -0
  40. package/dist/src/services/workflow/pipeline-verify-service.js +49 -4
  41. package/dist/src/services/workflow/plan-reader.d.ts +29 -0
  42. package/dist/src/services/workflow/plan-reader.js +158 -0
  43. package/dist/src/services/workflow/plan-refresher.d.ts +32 -0
  44. package/dist/src/services/workflow/plan-refresher.js +353 -0
  45. package/dist/src/services/workflow/plan-trigger-detector.d.ts +55 -0
  46. package/dist/src/services/workflow/plan-trigger-detector.js +142 -0
  47. package/dist/src/shared/version.d.ts +1 -1
  48. package/dist/src/shared/version.js +1 -1
  49. package/package.json +3 -2
  50. package/schemas/doctor-report.schema.json +2 -2
  51. package/skills/peaks-qa/SKILL.md +25 -0
  52. package/skills/peaks-qa/references/qa-perf-test-plan.md +67 -0
  53. package/skills/peaks-qa/references/qa-security-test-plan.md +73 -0
  54. package/skills/peaks-qa/references/qa-transition-gates.md +13 -9
  55. package/skills/peaks-rd/SKILL.md +2 -2
  56. package/skills/peaks-rd/references/mandatory-perf-baseline.md +2 -0
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Source-of-truth helpers for `peaks skill scope`.
3
+ *
4
+ * The source-of-truth file `.peaks/scope/skills.json` is the canonical
5
+ * record of the user's scope intent. Adapters translate it to their
6
+ * IDE-native config; the CLI always reads back from this file on `--show`.
7
+ *
8
+ * Atomicity: every write goes through `.peaks-tmp` first, then `rename`
9
+ * (POSIX-atomic; on Windows `rename` is atomic for files on the same volume).
10
+ * See tech-doc-025 §3.1.
11
+ */
12
+ import { existsSync } from 'node:fs';
13
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
14
+ import { dirname, join } from 'node:path';
15
+ /** File name for the canonical source-of-truth. */
16
+ export const SCOPE_FILE_NAME = 'skills.json';
17
+ /** Resolve the canonical source-of-truth path for a project root. */
18
+ export function scopeFilePath(projectRoot) {
19
+ return join(projectRoot, '.peaks', 'scope', SCOPE_FILE_NAME);
20
+ }
21
+ /** Resolve the per-IDE companion file path (kebab-case). */
22
+ export function ideCompanionFilePath(projectRoot, ide) {
23
+ return join(projectRoot, '.peaks', 'scope', `${ide}-skills.json`);
24
+ }
25
+ /** Resolve the `.peaks/scope/` directory for a project root. */
26
+ export function scopeDir(projectRoot) {
27
+ return join(projectRoot, '.peaks', 'scope');
28
+ }
29
+ /**
30
+ * Read the source-of-truth scope config, or null if it does not exist.
31
+ * Returns null on parse error too — the caller decides whether to surface.
32
+ */
33
+ export async function readSourceOfTruth(projectRoot) {
34
+ const file = scopeFilePath(projectRoot);
35
+ if (!existsSync(file))
36
+ return null;
37
+ try {
38
+ const raw = await readFile(file, 'utf8');
39
+ return JSON.parse(raw);
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ /**
46
+ * Read the per-IDE companion file (`.peaks/scope/<ide>-skills.json`), or
47
+ * null if it does not exist or is unparseable.
48
+ */
49
+ export async function readIdeCompanion(projectRoot, ide) {
50
+ const file = ideCompanionFilePath(projectRoot, ide);
51
+ if (!existsSync(file))
52
+ return null;
53
+ try {
54
+ const raw = await readFile(file, 'utf8');
55
+ return JSON.parse(raw);
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ /**
62
+ * Write the canonical source-of-truth atomically. The `.peaks-tmp` file
63
+ * is cleaned up in a finally block on partial failure.
64
+ */
65
+ export async function writeSourceOfTruth(projectRoot, config) {
66
+ const file = scopeFilePath(projectRoot);
67
+ await mkdir(scopeDir(projectRoot), { recursive: true });
68
+ const tmp = `${file}.peaks-tmp`;
69
+ try {
70
+ await writeFile(tmp, JSON.stringify(config, null, 2) + '\n', 'utf8');
71
+ await rename(tmp, file);
72
+ return file;
73
+ }
74
+ catch (error) {
75
+ if (existsSync(tmp)) {
76
+ try {
77
+ await rm(tmp, { force: true });
78
+ }
79
+ catch { /* best-effort */ }
80
+ }
81
+ throw error;
82
+ }
83
+ }
84
+ /**
85
+ * Write a generic JSON document atomically. Used by stub adapters for
86
+ * their `<ide>-skills.json` companion file (G3 §4.1).
87
+ */
88
+ export async function writeJsonAtomic(file, data) {
89
+ await mkdir(dirname(file), { recursive: true });
90
+ const tmp = `${file}.peaks-tmp`;
91
+ try {
92
+ await writeFile(tmp, JSON.stringify(data, null, 2) + '\n', 'utf8');
93
+ await rename(tmp, file);
94
+ }
95
+ catch (error) {
96
+ if (existsSync(tmp)) {
97
+ try {
98
+ await rm(tmp, { force: true });
99
+ }
100
+ catch { /* best-effort */ }
101
+ }
102
+ throw error;
103
+ }
104
+ }
105
+ /**
106
+ * Remove a file if it exists. Returns true if it was removed.
107
+ */
108
+ export async function removeIfExists(file) {
109
+ if (!existsSync(file))
110
+ return false;
111
+ try {
112
+ await rm(file, { force: true });
113
+ return true;
114
+ }
115
+ catch {
116
+ return false;
117
+ }
118
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * `peaks skill scope` — slice 025 multi-IDE skill scoping types.
3
+ *
4
+ * This file is the single source of truth for the `SkillScopeAdapter`
5
+ * interface (G1). It is consumed by every per-IDE adapter (Claude Code
6
+ * full impl, 5 stub adapters) and by the detection algorithm + CLI.
7
+ *
8
+ * Design notes (see tech-doc-025 §2):
9
+ * - The interface is deliberately small: only the four operations the CLI
10
+ * actually needs (detect / apply / show / reset).
11
+ * - `detect()` returns a confidence score in [0, 1] so the registry can
12
+ * pick the best match when several adapters are partially active.
13
+ * - Errors are typed (NotSupportedError / ScopeApplyError), not strings.
14
+ * The CLI maps `ScopeApplyError.code` to exit codes (see tech-doc §6.3).
15
+ */
16
+ import type { IdeId } from '../ide/ide-types.js';
17
+ /** Detection bucket for a single installed skill (G5). */
18
+ export type SkillRelevance = 'relevant' | 'borderline' | 'irrelevant';
19
+ /** Skill category — used for the JSON envelope's `kind` field (AC1). */
20
+ export type SkillKind = 'peaks-family' | 'generic-ai' | 'language-specific' | 'other';
21
+ /** Per-skill detail emitted by the detect algorithm. */
22
+ export interface SkillScopeRecord {
23
+ /** Skill directory name (e.g. "peaks-rd", "tdd-guide"). */
24
+ readonly name: string;
25
+ /** Category — peaks-family, generic-ai, language-specific, other. */
26
+ readonly kind: SkillKind;
27
+ /** Detection bucket. */
28
+ readonly relevance: SkillRelevance;
29
+ /** Human-readable reasons that produced the bucket; stable for fixtures. */
30
+ readonly reasons: readonly string[];
31
+ }
32
+ /** Counts of skills per bucket, for the JSON envelope (AC1). */
33
+ export interface SkillScopeCounts {
34
+ readonly relevant: number;
35
+ readonly borderline: number;
36
+ readonly irrelevant: number;
37
+ }
38
+ /** Project signals extracted from package.json + tsconfig + file tree (G5). */
39
+ export interface ProjectSignals {
40
+ /** True when the project's root package.json exists. */
41
+ readonly hasPackageJson: boolean;
42
+ readonly isTypeScript: boolean;
43
+ readonly isTypeScriptESM: boolean;
44
+ readonly isReact: boolean;
45
+ readonly isVue: boolean;
46
+ readonly isSvelte: boolean;
47
+ readonly isNext: boolean;
48
+ readonly isNestJS: boolean;
49
+ readonly isExpress: boolean;
50
+ readonly isFastify: boolean;
51
+ readonly isPostgres: boolean;
52
+ readonly isMysql: boolean;
53
+ readonly isMongo: boolean;
54
+ readonly isRedis: boolean;
55
+ readonly isDocker: boolean;
56
+ readonly isK8s: boolean;
57
+ readonly isCommander: boolean;
58
+ readonly isCodegraph: boolean;
59
+ readonly isHeadroom: boolean;
60
+ /** True when the project is a Python project (no package.json / has pyproject). */
61
+ readonly isPython: boolean;
62
+ /** Major version of Node engine requirement, or null. */
63
+ readonly nodeEngineMajor: number | null;
64
+ /** Top file extensions under src/ (max 50, lexicographically sorted, unique). */
65
+ readonly topExtensions: readonly string[];
66
+ /** Per-extension presence flags derived from topExtensions. */
67
+ readonly hasFileExtension: Readonly<Record<string, boolean>>;
68
+ }
69
+ /** The shape of the always-written source-of-truth file. */
70
+ export interface ScopeConfig {
71
+ /** ISO-8601 UTC timestamp at which this scope was last applied. */
72
+ readonly generatedAt: string;
73
+ /** Detected or explicitly-selected IDE id. */
74
+ readonly ide: IdeId;
75
+ /** Strictness mode (drives borderline handling). */
76
+ readonly strict: boolean;
77
+ /** Skills the user wants available (LLM-invokable). Always includes all peaks-*. */
78
+ readonly allowlist: readonly string[];
79
+ /** Skills the user wants hidden. */
80
+ readonly denylist: readonly string[];
81
+ /** Per-skill reasons (mirrored from detect, for audit). */
82
+ readonly skills: readonly SkillScopeRecord[];
83
+ /** Project signals that drove the classification. */
84
+ readonly signals: ProjectSignals;
85
+ }
86
+ /** Returned from `applyScope`. */
87
+ export interface ApplyResult {
88
+ /** Adapter id that handled the apply. */
89
+ readonly ide: IdeId;
90
+ /** Whether the apply succeeded (false = NOT_SUPPORTED or hard failure). */
91
+ readonly ok: boolean;
92
+ /** Absolute paths the adapter wrote or removed. */
93
+ readonly writtenFiles: readonly string[];
94
+ /** Whether shadow stubs were used (Claude Code fallback path). */
95
+ readonly usedShadowStub: boolean;
96
+ /** Whether the adapter returned NOT_SUPPORTED and only wrote the source-of-truth. */
97
+ readonly notSupported: boolean;
98
+ /** Peaks-* skills the adapter stripped from the denylist (G6 enforcement report). */
99
+ readonly strippedFromDenylist?: readonly string[];
100
+ /** Optional error code when ok=false. */
101
+ readonly error?: {
102
+ readonly code: string;
103
+ readonly message: string;
104
+ };
105
+ }
106
+ /** Returned from `showScope`. */
107
+ export interface ShowScopeResult {
108
+ /** The source-of-truth config, or null if no scope has been applied. */
109
+ readonly source: ScopeConfig | null;
110
+ /** Whatever the adapter can read back from its native config. Null if not supported. */
111
+ readonly native: unknown;
112
+ /** Adapter id. */
113
+ readonly ide: IdeId;
114
+ }
115
+ /** Reset output mirrors apply but without the lists. */
116
+ export interface ResetScopeResult {
117
+ readonly ide: IdeId;
118
+ readonly removedFiles: readonly string[];
119
+ }
120
+ /** Sentinel error type for stub adapters (G3). */
121
+ export declare class NotSupportedError extends Error {
122
+ readonly code: "NOT_SUPPORTED";
123
+ readonly ide: IdeId;
124
+ constructor(ide: IdeId, message: string);
125
+ }
126
+ /** Errors emitted by adapters (validated by runtime probe in claude-code §3.4). */
127
+ export type ScopeApplyErrorCode = 'NOT_SUPPORTED' | 'IO_ERROR' | 'MALFORMED_CONFIG' | 'WRITE_FAILED' | 'PARTIAL_FAILURE';
128
+ export declare class ScopeApplyError extends Error {
129
+ readonly code: ScopeApplyErrorCode;
130
+ readonly ide: IdeId;
131
+ constructor(code: ScopeApplyErrorCode, message: string, ide: IdeId);
132
+ }
133
+ /** Input to `applyScope`. */
134
+ export interface ApplyScopeInput {
135
+ /** Final allowlist (the CLI guarantees peaks-* is in here before calling). */
136
+ readonly allowlist: readonly string[];
137
+ /** Final denylist. */
138
+ readonly denylist: readonly string[];
139
+ /** Strictness mode. */
140
+ readonly strict: boolean;
141
+ /** Project root for resolving relative paths. */
142
+ readonly projectRoot: string;
143
+ /** Source-of-truth config that the adapter MAY re-derive fields from. */
144
+ readonly sourceConfig: ScopeConfig;
145
+ /** When true, prefer shadow-stub fallback over the native config. */
146
+ readonly shadowFallback: boolean;
147
+ /** Test seam: simulate an adapter write failure (returns the partial path written before failure). */
148
+ readonly simulateWriteFailure?: boolean;
149
+ }
150
+ /** Reset input mirrors apply but without the lists. */
151
+ export interface ResetScopeInput {
152
+ readonly projectRoot: string;
153
+ }
154
+ /** The interface every adapter implements. */
155
+ export interface SkillScopeAdapter {
156
+ /** Adapter id; matches the IdeId it pairs with. */
157
+ readonly ide: IdeId;
158
+ /** Whether this adapter supports a real (non-stub) implementation. */
159
+ readonly supported: boolean;
160
+ /** Detect this adapter's IDE is active in the given project root. Returns a confidence score in [0,1]. */
161
+ detect(projectRoot: string): Promise<number>;
162
+ /** Write the IDE-specific scope config. */
163
+ applyScope(input: ApplyScopeInput): Promise<ApplyResult>;
164
+ /** Read the current scope config. */
165
+ showScope(projectRoot: string): Promise<ShowScopeResult>;
166
+ /** Remove the IDE-specific scope config. */
167
+ resetScope(input: ResetScopeInput): Promise<ResetScopeResult>;
168
+ }
169
+ /** Helper: build a NOT_SUPPORTED ApplyResult (for stub adapters that pre-write source-of-truth). */
170
+ export declare function makeNotSupportedResult(ide: IdeId, message: string, writtenFiles?: readonly string[]): ApplyResult;
171
+ /** Hard-coded allowlist of peaks-* skills (G6 + generic AI-engineering skills per AC2). */
172
+ export declare const ALWAYS_RELEVANT_SKILLS: readonly string[];
173
+ /** Hard-coded denylist prefixes: non-TS language families (G5 §5.4). */
174
+ export declare const NON_TS_SKILL_PREFIXES: readonly string[];
175
+ /** File extensions the file-tree walker looks for (top-50 limit per tech-doc §5.1). */
176
+ export declare const TRACKED_EXTENSIONS: readonly string[];
@@ -0,0 +1,74 @@
1
+ /**
2
+ * `peaks skill scope` — slice 025 multi-IDE skill scoping types.
3
+ *
4
+ * This file is the single source of truth for the `SkillScopeAdapter`
5
+ * interface (G1). It is consumed by every per-IDE adapter (Claude Code
6
+ * full impl, 5 stub adapters) and by the detection algorithm + CLI.
7
+ *
8
+ * Design notes (see tech-doc-025 §2):
9
+ * - The interface is deliberately small: only the four operations the CLI
10
+ * actually needs (detect / apply / show / reset).
11
+ * - `detect()` returns a confidence score in [0, 1] so the registry can
12
+ * pick the best match when several adapters are partially active.
13
+ * - Errors are typed (NotSupportedError / ScopeApplyError), not strings.
14
+ * The CLI maps `ScopeApplyError.code` to exit codes (see tech-doc §6.3).
15
+ */
16
+ /** Sentinel error type for stub adapters (G3). */
17
+ export class NotSupportedError extends Error {
18
+ code = 'NOT_SUPPORTED';
19
+ ide;
20
+ constructor(ide, message) {
21
+ super(`${ide}: ${message}`);
22
+ this.name = 'NotSupportedError';
23
+ this.ide = ide;
24
+ }
25
+ }
26
+ export class ScopeApplyError extends Error {
27
+ code;
28
+ ide;
29
+ constructor(code, message, ide) {
30
+ super(`${ide}: ${message}`);
31
+ this.name = 'ScopeApplyError';
32
+ this.code = code;
33
+ this.ide = ide;
34
+ }
35
+ }
36
+ /** Helper: build a NOT_SUPPORTED ApplyResult (for stub adapters that pre-write source-of-truth). */
37
+ export function makeNotSupportedResult(ide, message, writtenFiles = []) {
38
+ return {
39
+ ide,
40
+ ok: false,
41
+ writtenFiles: [...writtenFiles],
42
+ usedShadowStub: false,
43
+ notSupported: true,
44
+ error: { code: 'NOT_SUPPORTED', message },
45
+ };
46
+ }
47
+ /** Hard-coded allowlist of peaks-* skills (G6 + generic AI-engineering skills per AC2). */
48
+ export const ALWAYS_RELEVANT_SKILLS = [
49
+ // peaks-* family (G6 hard constraint)
50
+ 'peaks-rd', 'peaks-qa', 'peaks-solo', 'peaks-prd', 'peaks-sc',
51
+ 'peaks-txt', 'peaks-sop', 'peaks-solo-resume', 'peaks-solo-status',
52
+ 'peaks-solo-test', 'peaks-ui', 'peaks-ide',
53
+ // generic AI-engineering skills (per AC2)
54
+ 'tdd-guide', 'coding-standards', 'karpathy-guidelines',
55
+ 'continuous-learning', 'code-tour', 'agent-harness-construction',
56
+ 'security-review', 'code-review',
57
+ ];
58
+ /** Hard-coded denylist prefixes: non-TS language families (G5 §5.4). */
59
+ export const NON_TS_SKILL_PREFIXES = [
60
+ 'kotlin-', 'python-', 'java-', 'rust-', 'go-', 'ruby-',
61
+ 'swift-', 'csharp-', 'cpp-',
62
+ ];
63
+ /** File extensions the file-tree walker looks for (top-50 limit per tech-doc §5.1). */
64
+ export const TRACKED_EXTENSIONS = [
65
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
66
+ '.swift', '.kt', '.kts', '.java', '.scala',
67
+ '.py', '.pyx', '.go', '.rs',
68
+ '.rb', '.php', '.cs', '.cpp', '.c', '.h', '.hpp',
69
+ '.vue', '.svelte', '.html', '.css', '.scss',
70
+ '.json', '.yaml', '.yml', '.toml', '.md',
71
+ '.sql', '.sh', '.bash', '.ps1',
72
+ '.dockerfile', '.dockerignore', '.lua', '.ex', '.exs', '.erl', '.hs',
73
+ '.dart', '.r', '.jl', '.clj',
74
+ ];
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Slice 028 (Q2=A): `peaks standards migrate` rewrites a consumer
3
+ * project's `CLAUDE.md` to drop the legacy heartbeat block.
4
+ *
5
+ * The legacy block (rendered by `peaks standards init` / `peaks
6
+ * standards update` before slice 028) contained instructions that:
7
+ * 1. pointed the LLM at the legacy `.peaks/.active-skill.json` path;
8
+ * 2. required the LLM to invoke `peaks skill heartbeat:touch` and
9
+ * `peaks skill presence:clear` on every turn;
10
+ * 3. ended with an `External reference: https://github.com/affaan-m/...`
11
+ * line that was peaks-cli-internal, not consumer-facing.
12
+ *
13
+ * The replacement text matches the post-slice-025 peaks-cli repo's own
14
+ * `CLAUDE.md`: route the LLM through `peaks skill presence --json` and
15
+ * render a compact status header when a valid skill is active. This
16
+ * service is the deterministic in-place rewriter that brings existing
17
+ * consumer trees in line with the new template.
18
+ *
19
+ * Behavior:
20
+ * - `migrateStandards({ project, apply: true })` — rewrites the
21
+ * file when the legacy block is present, returns
22
+ * `applied: true`.
23
+ * - `migrateStandards({ project, dryRun: true })` — returns the
24
+ * would-change preview, no write.
25
+ * - `migrateStandards({ project })` — defaults to a
26
+ * dry-run. `--apply` is the only opt-in for a destructive write.
27
+ * - File missing → `file: null`, no throw.
28
+ * - Legacy block not present → `foundOldBlock: false`, no write.
29
+ */
30
+ export declare const NEW_TEMPLATE_TEXT = "Active Peaks-Cli skill presence: at the start of every response, run `peaks skill presence --json` to read the active skill marker. The CLI handles canonical-path resolution (`.peaks/_runtime/active-skill.json` with back-compat fallback to `.peaks/.active-skill.json`); do not read those files directly. When the response includes a valid skill name, display the compact status header: `Peaks-Cli Skill: <skill> | Peaks-Cli Gate: <gate> | Next: <one short action>`. Display the header on every turn while the CLI returns an active skill; omit when the CLI returns no active skill.";
31
+ export type MigrateStandardsInput = {
32
+ project: string;
33
+ apply?: boolean;
34
+ dryRun?: boolean;
35
+ };
36
+ export type MigrateStandardsData = {
37
+ file: string | null;
38
+ foundOldBlock: boolean;
39
+ wouldChange: boolean;
40
+ applied: boolean;
41
+ before: {
42
+ lines: number;
43
+ } | null;
44
+ after: {
45
+ lines: number;
46
+ } | null;
47
+ nextActions: string[];
48
+ };
49
+ export type MigrateStandardsResult = {
50
+ ok: true;
51
+ data: MigrateStandardsData;
52
+ warnings: string[];
53
+ };
54
+ export declare function detectLegacyBlock(content: string): {
55
+ found: boolean;
56
+ start: number;
57
+ end: number;
58
+ };
59
+ export declare function rewriteLegacyBlock(content: string, newText?: string): {
60
+ rewritten: string;
61
+ replaced: boolean;
62
+ };
63
+ export declare function migrateStandards(input: MigrateStandardsInput): MigrateStandardsResult;
@@ -0,0 +1,193 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { isAbsolute, resolve } from 'node:path';
3
+ /**
4
+ * Slice 028 (Q2=A): `peaks standards migrate` rewrites a consumer
5
+ * project's `CLAUDE.md` to drop the legacy heartbeat block.
6
+ *
7
+ * The legacy block (rendered by `peaks standards init` / `peaks
8
+ * standards update` before slice 028) contained instructions that:
9
+ * 1. pointed the LLM at the legacy `.peaks/.active-skill.json` path;
10
+ * 2. required the LLM to invoke `peaks skill heartbeat:touch` and
11
+ * `peaks skill presence:clear` on every turn;
12
+ * 3. ended with an `External reference: https://github.com/affaan-m/...`
13
+ * line that was peaks-cli-internal, not consumer-facing.
14
+ *
15
+ * The replacement text matches the post-slice-025 peaks-cli repo's own
16
+ * `CLAUDE.md`: route the LLM through `peaks skill presence --json` and
17
+ * render a compact status header when a valid skill is active. This
18
+ * service is the deterministic in-place rewriter that brings existing
19
+ * consumer trees in line with the new template.
20
+ *
21
+ * Behavior:
22
+ * - `migrateStandards({ project, apply: true })` — rewrites the
23
+ * file when the legacy block is present, returns
24
+ * `applied: true`.
25
+ * - `migrateStandards({ project, dryRun: true })` — returns the
26
+ * would-change preview, no write.
27
+ * - `migrateStandards({ project })` — defaults to a
28
+ * dry-run. `--apply` is the only opt-in for a destructive write.
29
+ * - File missing → `file: null`, no throw.
30
+ * - Legacy block not present → `foundOldBlock: false`, no write.
31
+ */
32
+ export const NEW_TEMPLATE_TEXT = 'Active Peaks-Cli skill presence: at the start of every response, run `peaks skill presence --json` to read the active skill marker. The CLI handles canonical-path resolution (`.peaks/_runtime/active-skill.json` with back-compat fallback to `.peaks/.active-skill.json`); do not read those files directly. When the response includes a valid skill name, display the compact status header: `Peaks-Cli Skill: <skill> | Peaks-Cli Gate: <gate> | Next: <one short action>`. Display the header on every turn while the CLI returns an active skill; omit when the CLI returns no active skill.';
33
+ const LEGACY_BLOCK_OPENER_LINE = 'Peaks-Cli 心跳检测 (heartbeat check)';
34
+ const LEGACY_BLOCK_CLOSER = 'External reference: https://github.com/affaan-m/everything-claude-code';
35
+ const LEGACY_MARKER_FALLBACK = 'Do NOT skip step 3-5. The CLI heartbeat:touch command';
36
+ const FORBIDDEN_LEGACY_STRINGS = [
37
+ 'heartbeat:touch',
38
+ 'presence:clear',
39
+ 'Default runbook',
40
+ 'Startup sequence',
41
+ 'Swarm parallel phase'
42
+ ];
43
+ const NEW_TEMPLATE_FINGERPRINT = 'peaks skill presence --json';
44
+ export function detectLegacyBlock(content) {
45
+ const openerIndex = content.indexOf(LEGACY_BLOCK_OPENER_LINE);
46
+ if (openerIndex >= 0) {
47
+ // Walk back to the start of the `<!--` line. The opener text is
48
+ // typically indented on the line after `<!--` (the legacy block
49
+ // is a multi-line HTML comment), so we cut from the most recent
50
+ // `<!--` line above. If no `<!--` is found in the surrounding
51
+ // 4 lines, fall back to the start of the opener's own line.
52
+ const htmlCommentIndex = content.lastIndexOf('<!--', openerIndex);
53
+ const previousNewlineBeforeOpener = content.lastIndexOf('\n', openerIndex);
54
+ let start;
55
+ if (htmlCommentIndex < 0 || htmlCommentIndex < previousNewlineBeforeOpener - 200) {
56
+ // No `<!--` line within a reasonable distance — start of the
57
+ // opener's own line.
58
+ start = previousNewlineBeforeOpener + 1;
59
+ }
60
+ else {
61
+ start = content.lastIndexOf('\n', htmlCommentIndex) + 1;
62
+ }
63
+ const closerIndex = content.indexOf(LEGACY_BLOCK_CLOSER, openerIndex);
64
+ let endIndex;
65
+ if (closerIndex < 0) {
66
+ const tailIndex = content.indexOf(LEGACY_MARKER_FALLBACK, openerIndex);
67
+ if (tailIndex < 0) {
68
+ endIndex = content.length;
69
+ }
70
+ else {
71
+ endIndex = content.indexOf('\n', tailIndex);
72
+ if (endIndex < 0)
73
+ endIndex = content.length;
74
+ }
75
+ }
76
+ else {
77
+ endIndex = content.indexOf('\n', closerIndex);
78
+ if (endIndex < 0)
79
+ endIndex = content.length;
80
+ }
81
+ return { found: true, start, end: endIndex };
82
+ }
83
+ // Fallback: opener stripped by editor / re-format. Detect by the
84
+ // first forbidden string that survives the rewrite, then walk back
85
+ // to the start of the line.
86
+ for (const marker of FORBIDDEN_LEGACY_STRINGS) {
87
+ const idx = content.indexOf(marker);
88
+ if (idx >= 0) {
89
+ const start = content.lastIndexOf('\n', idx) + 1;
90
+ return { found: true, start, end: content.length };
91
+ }
92
+ }
93
+ return { found: false, start: -1, end: -1 };
94
+ }
95
+ export function rewriteLegacyBlock(content, newText = NEW_TEMPLATE_TEXT) {
96
+ const detection = detectLegacyBlock(content);
97
+ if (!detection.found) {
98
+ return { rewritten: content, replaced: false };
99
+ }
100
+ const before = content.slice(0, detection.start);
101
+ const after = content.slice(detection.end);
102
+ const trimmedBefore = before.replace(/\s+$/u, '\n');
103
+ const cleanedAfter = after.replace(/^\n+/u, '\n');
104
+ const rewritten = `${trimmedBefore}${newText}${cleanedAfter}`;
105
+ return { rewritten, replaced: true };
106
+ }
107
+ export function migrateStandards(input) {
108
+ const project = input.project;
109
+ const projectRoot = isAbsolute(project) ? project : resolve(project);
110
+ const filePath = resolve(projectRoot, 'CLAUDE.md');
111
+ const apply = input.apply === true;
112
+ const dryRun = input.dryRun === true || apply === false;
113
+ if (!existsSync(filePath)) {
114
+ return {
115
+ ok: true,
116
+ data: {
117
+ file: null,
118
+ foundOldBlock: false,
119
+ wouldChange: false,
120
+ applied: false,
121
+ before: null,
122
+ after: null,
123
+ nextActions: ['CLAUDE.md does not exist; nothing to migrate']
124
+ },
125
+ warnings: []
126
+ };
127
+ }
128
+ const original = readFileSync(filePath, 'utf8');
129
+ const detection = detectLegacyBlock(original);
130
+ if (!detection.found) {
131
+ if (original.includes(NEW_TEMPLATE_FINGERPRINT)) {
132
+ return {
133
+ ok: true,
134
+ data: {
135
+ file: filePath,
136
+ foundOldBlock: false,
137
+ wouldChange: false,
138
+ applied: false,
139
+ before: { lines: original.split('\n').length },
140
+ after: null,
141
+ nextActions: ['CLAUDE.md is already up to date']
142
+ },
143
+ warnings: []
144
+ };
145
+ }
146
+ return {
147
+ ok: true,
148
+ data: {
149
+ file: filePath,
150
+ foundOldBlock: false,
151
+ wouldChange: false,
152
+ applied: false,
153
+ before: { lines: original.split('\n').length },
154
+ after: null,
155
+ nextActions: ['CLAUDE.md has no peaks-cli block; nothing to migrate']
156
+ },
157
+ warnings: []
158
+ };
159
+ }
160
+ const { rewritten } = rewriteLegacyBlock(original);
161
+ const nextActions = [];
162
+ if (dryRun) {
163
+ nextActions.push('Re-run with --apply to perform the rewrite');
164
+ return {
165
+ ok: true,
166
+ data: {
167
+ file: filePath,
168
+ foundOldBlock: true,
169
+ wouldChange: true,
170
+ applied: false,
171
+ before: { lines: original.split('\n').length },
172
+ after: { lines: rewritten.split('\n').length },
173
+ nextActions
174
+ },
175
+ warnings: []
176
+ };
177
+ }
178
+ writeFileSync(filePath, rewritten, 'utf8');
179
+ nextActions.push('CLAUDE.md rewritten; no further action required');
180
+ return {
181
+ ok: true,
182
+ data: {
183
+ file: filePath,
184
+ foundOldBlock: true,
185
+ wouldChange: true,
186
+ applied: true,
187
+ before: { lines: original.split('\n').length },
188
+ after: { lines: rewritten.split('\n').length },
189
+ nextActions
190
+ },
191
+ warnings: []
192
+ };
193
+ }
@@ -134,29 +134,7 @@ function renderClaudeMd(language, ctx) {
134
134
  '- Read `.claude/rules/common/security.md` before touching filesystem, user input, external calls, auth, or secrets.',
135
135
  `- Read .claude/rules/${language}/coding-style.md for language-specific standards when applicable.`,
136
136
  '',
137
- '<!--',
138
- ' Peaks-Cli 心跳检测 (heartbeat check)',
139
- ' LLM 每轮回复前必须执行以下步骤,由 CLI 命令强制执行:',
140
- ' 1. 读取 .peaks/.active-skill.json → 存在则显示 status header',
141
- ' 2. 扫描当前上下文查找 peaks 技能标记',
142
- ' ("Peaks-Cli Gate", "Peaks-Cli Default runbook", "Peaks-Cli Startup sequence")',
143
- ' 3. 标记存在 → 调用 `peaks skill heartbeat:touch` 记录心跳',
144
- ' 4. 标记不存在 → 显示警告 + 调用 `peaks skill presence:clear` 关闭心跳',
145
- '-->',
146
- 'Active Peaks-Cli skill presence: at the start of EVERY response, run these steps in order:',
147
- '',
148
- '1. Read `.peaks/.active-skill.json`. If missing or invalid, skip all remaining steps.',
149
- '2. Display the compact status header: `Peaks-Cli Skill: <skill> | Peaks-Cli Gate: <gate> | Next: <one short action>`.',
150
- '3. Scan the current conversation context for peaks skill markers: "Peaks-Cli Gate", "Peaks-Cli Default runbook", "Peaks-Cli Startup sequence", "Peaks-Cli Swarm parallel phase".',
151
- '4. If at least one marker IS found → run `peaks skill heartbeat:touch` to record a successful heartbeat. The CLI command writes the timestamp and returns JSON — verify it succeeded.',
152
- '5. If NO markers are found → the skill context has been lost (context compaction). Run `peaks skill presence:clear` to close the heartbeat, then display this warning:',
153
- '',
154
- '> ⚠ Peaks-Cli: skill presence file was active but skill context has been lost from the conversation. The workflow can no longer run correctly. Please re-invoke the relevant /peaks-* skill to reload the full skill instructions.',
155
- '',
156
- 'Do NOT skip step 3-5. The CLI heartbeat:touch command is the mechanism that makes heartbeat auditable — failing to call it means the heartbeat is broken.',
157
- '',
158
- 'External reference: https://github.com/affaan-m/everything-claude-code is used as a curated reference only. Do not execute or install external content without explicit approval.',
159
- ''
137
+ 'Active Peaks-Cli skill presence: at the start of every response, run `peaks skill presence --json` to read the active skill marker. The CLI handles canonical-path resolution (`.peaks/_runtime/active-skill.json` with back-compat fallback to `.peaks/.active-skill.json`); do not read those files directly. When the response includes a valid skill name, display the compact status header: `Peaks-Cli Skill: <skill> | Peaks-Cli Gate: <gate> | Next: <one short action>`. Display the header on every turn while the CLI returns an active skill; omit when the CLI returns no active skill.'
160
138
  ].join('\n');
161
139
  const stack = renderProjectStackSection(ctx);
162
140
  return stack === '' ? head : `${head}\n${stack}`;