peaks-cli 1.3.1 → 1.3.3
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/README.md +6 -2
- package/bin/peaks.js +0 -0
- package/dist/src/cli/commands/core-artifact-commands.js +49 -11
- package/dist/src/cli/commands/gate-commands.js +28 -19
- package/dist/src/cli/commands/hook-handle.d.ts +17 -0
- package/dist/src/cli/commands/hook-handle.js +111 -0
- package/dist/src/cli/commands/hooks-commands.js +72 -21
- package/dist/src/cli/commands/progress-commands.js +9 -2
- package/dist/src/cli/commands/progress-start-spawn.js +30 -4
- package/dist/src/cli/commands/slice-commands.js +4 -2
- package/dist/src/cli/commands/statusline-commands.js +75 -17
- package/dist/src/cli/commands/sub-agent-commands.d.ts +5 -0
- package/dist/src/cli/commands/sub-agent-commands.js +488 -0
- package/dist/src/cli/commands/sub-agent-dispatch-guard.d.ts +55 -0
- package/dist/src/cli/commands/sub-agent-dispatch-guard.js +57 -0
- package/dist/src/cli/commands/workspace-commands.js +70 -14
- package/dist/src/cli/program.js +9 -0
- package/dist/src/hooks/pre-tool-use-sub-agent.d.ts +28 -0
- package/dist/src/hooks/pre-tool-use-sub-agent.js +105 -0
- package/dist/src/services/artifacts/artifact-prerequisites.d.ts +12 -0
- package/dist/src/services/artifacts/artifact-prerequisites.js +39 -8
- package/dist/src/services/artifacts/request-artifact-service.js +116 -76
- package/dist/src/services/config/config-types.d.ts +1 -1
- package/dist/src/services/context/artifact-meta.d.ts +72 -0
- package/dist/src/services/context/artifact-meta.js +105 -0
- package/dist/src/services/context/context-guard.d.ts +49 -0
- package/dist/src/services/context/context-guard.js +91 -0
- package/dist/src/services/context/dispatch-context-guard.d.ts +27 -0
- package/dist/src/services/context/dispatch-context-guard.js +192 -0
- package/dist/src/services/context/headroom-client.d.ts +34 -0
- package/dist/src/services/context/headroom-client.js +117 -0
- package/dist/src/services/context/shared-channel.d.ts +92 -0
- package/dist/src/services/context/shared-channel.js +285 -0
- package/dist/src/services/context/threshold.d.ts +35 -0
- package/dist/src/services/context/threshold.js +76 -0
- package/dist/src/services/dispatch/batch-counter.d.ts +27 -0
- package/dist/src/services/dispatch/batch-counter.js +85 -0
- package/dist/src/services/dispatch/dispatch-record-writer.d.ts +93 -0
- package/dist/src/services/dispatch/dispatch-record-writer.js +261 -0
- package/dist/src/services/dispatch/heartbeat-truncator.d.ts +26 -0
- package/dist/src/services/dispatch/heartbeat-truncator.js +13 -0
- package/dist/src/services/dispatch/leak-detector.d.ts +11 -0
- package/dist/src/services/dispatch/leak-detector.js +72 -0
- package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +127 -0
- package/dist/src/services/dispatch/sub-agent-dispatcher.js +98 -0
- package/dist/src/services/doctor/doctor-service.d.ts +62 -0
- package/dist/src/services/doctor/doctor-service.js +276 -1
- package/dist/src/services/ide/adapters/claude-code-adapter.d.ts +18 -0
- package/dist/src/services/ide/adapters/claude-code-adapter.js +53 -0
- package/dist/src/services/ide/adapters/trae-adapter.d.ts +34 -0
- package/dist/src/services/ide/adapters/trae-adapter.js +70 -0
- package/dist/src/services/ide/hook-protocol.d.ts +44 -0
- package/dist/src/services/ide/hook-protocol.js +71 -0
- package/dist/src/services/ide/hook-translator.d.ts +72 -0
- package/dist/src/services/ide/hook-translator.js +128 -0
- package/dist/src/services/ide/ide-detector.d.ts +10 -0
- package/dist/src/services/ide/ide-detector.js +19 -0
- package/dist/src/services/ide/ide-registry.d.ts +14 -0
- package/dist/src/services/ide/ide-registry.js +45 -0
- package/dist/src/services/ide/ide-types.d.ts +120 -0
- package/dist/src/services/ide/ide-types.js +2 -0
- package/dist/src/services/ide/shared/atomic-json.d.ts +15 -0
- package/dist/src/services/ide/shared/atomic-json.js +58 -0
- package/dist/src/services/ide/shared/safe-path.d.ts +11 -0
- package/dist/src/services/ide/shared/safe-path.js +29 -0
- package/dist/src/services/progress/progress-service.d.ts +1 -1
- package/dist/src/services/progress/progress-service.js +18 -14
- package/dist/src/services/security/safe-settings-path.d.ts +12 -0
- package/dist/src/services/security/safe-settings-path.js +104 -0
- package/dist/src/services/session/session-manager.d.ts +22 -1
- package/dist/src/services/session/session-manager.js +137 -28
- package/dist/src/services/signal/cancel-handler.d.ts +14 -0
- package/dist/src/services/signal/cancel-handler.js +76 -0
- package/dist/src/services/skill/resume-detector.d.ts +54 -0
- package/dist/src/services/skill/resume-detector.js +334 -0
- package/dist/src/services/skill/skill-scheduler.d.ts +40 -0
- package/dist/src/services/skill/skill-scheduler.js +53 -0
- package/dist/src/services/skills/hooks-settings-service.d.ts +47 -29
- package/dist/src/services/skills/hooks-settings-service.js +190 -144
- package/dist/src/services/skills/statusline-settings-service.d.ts +33 -6
- package/dist/src/services/skills/statusline-settings-service.js +31 -34
- package/dist/src/services/slice/slice-archive-service.d.ts +20 -0
- package/dist/src/services/slice/slice-archive-service.js +111 -0
- package/dist/src/services/slice/slice-check-service.js +20 -1
- package/dist/src/services/slice/slice-check-types.d.ts +9 -0
- package/dist/src/services/solo/batch-heartbeat-poller.d.ts +51 -0
- package/dist/src/services/solo/batch-heartbeat-poller.js +88 -0
- package/dist/src/services/solo/status-line-renderer.d.ts +34 -0
- package/dist/src/services/solo/status-line-renderer.js +55 -0
- package/dist/src/services/workspace/migrate-service.js +124 -2
- package/dist/src/services/workspace/migrate-types.d.ts +50 -7
- package/dist/src/services/workspace/reconcile-service.d.ts +69 -0
- package/dist/src/services/workspace/reconcile-service.js +267 -48
- package/dist/src/services/workspace/reconcile-types.d.ts +37 -0
- package/dist/src/services/workspace/workspace-service.js +29 -62
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +2 -1
- package/schemas/doctor-report.schema.json +2 -2
- package/skills/peaks-ide/SKILL.md +159 -0
- package/skills/peaks-qa/SKILL.md +58 -1
- package/skills/peaks-qa/references/qa-fanout-contract.md +150 -0
- package/skills/peaks-rd/SKILL.md +52 -9
- package/skills/peaks-solo/SKILL.md +83 -20
- package/skills/peaks-solo/references/context-governance.md +144 -0
- package/skills/peaks-solo/references/headroom-integration.md +107 -0
- package/skills/peaks-solo/references/runbook.md +3 -3
- package/skills/peaks-solo/references/sub-agent-dispatch.md +218 -0
- package/skills/peaks-solo/references/swarm-dispatch-contract.md +3 -37
- package/skills/peaks-txt/SKILL.md +19 -0
- package/skills/peaks-ui/SKILL.md +28 -1
|
@@ -18,6 +18,32 @@ export type CodegraphCapabilityProbe = {
|
|
|
18
18
|
binaryPath: string;
|
|
19
19
|
binaryExists: boolean;
|
|
20
20
|
};
|
|
21
|
+
export type DistVersionComparison = {
|
|
22
|
+
dist: string | null;
|
|
23
|
+
source: string;
|
|
24
|
+
match: boolean;
|
|
25
|
+
distReadable: boolean;
|
|
26
|
+
};
|
|
27
|
+
export type DistVersionProbe = () => DistVersionComparison;
|
|
28
|
+
export type WorkspaceLayoutInspection = {
|
|
29
|
+
topLevelSessionDirs: string[];
|
|
30
|
+
legacyDotfiles: string[];
|
|
31
|
+
/**
|
|
32
|
+
* Slice 007 — per-change-id top-level dirs (e.g. `.peaks/001-2026-06-06-.../`).
|
|
33
|
+
* The pre-F3 canonical layout put reviewable artifacts under a
|
|
34
|
+
* per-change-id top-level dir; the post-F3 canonical layout
|
|
35
|
+
* consolidates them under `.peaks/_runtime/<sid>/<role>/`. Any
|
|
36
|
+
* leftover per-change-id top-level dir is a regression to flag.
|
|
37
|
+
* Slice 008's migration will consolidate these; until then, the
|
|
38
|
+
* check reports them as `ok: false`.
|
|
39
|
+
*
|
|
40
|
+
* Optional in the type for back-compat with test probes that
|
|
41
|
+
* pre-date the slice 007 broadening; the check itself falls back
|
|
42
|
+
* to an empty array when the field is missing.
|
|
43
|
+
*/
|
|
44
|
+
perChangeIdDirs?: string[];
|
|
45
|
+
};
|
|
46
|
+
export type WorkspaceLayoutProbe = () => WorkspaceLayoutInspection;
|
|
21
47
|
export type DoctorOptions = {
|
|
22
48
|
schemasBaseDir?: string;
|
|
23
49
|
skillsBaseDir?: string;
|
|
@@ -29,6 +55,10 @@ export type DoctorOptions = {
|
|
|
29
55
|
workspaceInitializedProbe?: () => boolean;
|
|
30
56
|
/** Platform string (defaults to process.platform); injectable for tests. */
|
|
31
57
|
platform?: NodeJS.Platform;
|
|
58
|
+
/** Injected for the build:dist-version-matches-source check (defaults to compareDistVersion on disk). */
|
|
59
|
+
distVersionProbe?: DistVersionProbe;
|
|
60
|
+
/** Injected for the build:workspace-layout-canonical check (defaults to inspectWorkspaceLayout on disk). */
|
|
61
|
+
workspaceLayoutProbe?: WorkspaceLayoutProbe;
|
|
32
62
|
};
|
|
33
63
|
/**
|
|
34
64
|
* Pure helper extracted from `defaultWorkspaceInitializedProbe` so tests can
|
|
@@ -37,4 +67,36 @@ export type DoctorOptions = {
|
|
|
37
67
|
* `.peaks/_runtime/session.json` OR the legacy `.peaks/.session.json` exists.
|
|
38
68
|
*/
|
|
39
69
|
export declare function isWorkspaceInitializedAt(projectRoot: string): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Pure helper that compares the published dist `CLI_VERSION` against the
|
|
72
|
+
* source-of-truth `package.json#version`. Default readers fail-soft to `null`
|
|
73
|
+
* on missing/unreadable/malformed input. Exported so tests can drive the
|
|
74
|
+
* filesystem reads without monkey-patching `process.cwd()`.
|
|
75
|
+
*/
|
|
76
|
+
export declare function compareDistVersion(opts: {
|
|
77
|
+
projectRoot: string;
|
|
78
|
+
distVersionReader?: (root: string) => string | null;
|
|
79
|
+
sourceVersionReader?: (root: string) => string | null;
|
|
80
|
+
}): DistVersionComparison;
|
|
81
|
+
/**
|
|
82
|
+
* Pure helper that inspects the on-disk workspace layout for
|
|
83
|
+
* post-F3-canonical violations. The post-F3 canonical layout puts
|
|
84
|
+
* session dirs under `.peaks/_runtime/<sid>/` and the runtime
|
|
85
|
+
* binding at `.peaks/_runtime/session.json`; the legacy paths
|
|
86
|
+
* (top-level `<YYYY-MM-DD-session-<hex>>/` dirs and the legacy
|
|
87
|
+
* top-level `.peaks/.session.json` / `.peaks/.active-skill.json`
|
|
88
|
+
* dotfiles) must be absent. This helper is exported so tests can
|
|
89
|
+
* drive the filesystem walk without monkey-patching `process.cwd()`
|
|
90
|
+
* or `findProjectRoot`.
|
|
91
|
+
*
|
|
92
|
+
* Both scanners fail-soft (return `[]` on read errors) so a flaky
|
|
93
|
+
* filesystem read on a non-fatal probe path never escalates into a
|
|
94
|
+
* doctor failure.
|
|
95
|
+
*/
|
|
96
|
+
export declare function inspectWorkspaceLayout(opts: {
|
|
97
|
+
projectRoot: string;
|
|
98
|
+
topLevelScanner?: (root: string) => string[];
|
|
99
|
+
dotfileScanner?: (root: string) => string[];
|
|
100
|
+
perChangeIdScanner?: (root: string) => string[];
|
|
101
|
+
}): WorkspaceLayoutInspection;
|
|
40
102
|
export declare function runDoctor(options?: DoctorOptions): Promise<DoctorReport>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
3
|
+
import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import { dirname, resolve as resolvePath } from 'node:path';
|
|
6
6
|
import { readText } from '../../shared/fs.js';
|
|
@@ -68,6 +68,192 @@ export function isWorkspaceInitializedAt(projectRoot) {
|
|
|
68
68
|
return (existsSync(join(projectRoot, '.peaks', '_runtime', 'session.json')) ||
|
|
69
69
|
existsSync(join(projectRoot, '.peaks', '.session.json')));
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Pure helper that compares the published dist `CLI_VERSION` against the
|
|
73
|
+
* source-of-truth `package.json#version`. Default readers fail-soft to `null`
|
|
74
|
+
* on missing/unreadable/malformed input. Exported so tests can drive the
|
|
75
|
+
* filesystem reads without monkey-patching `process.cwd()`.
|
|
76
|
+
*/
|
|
77
|
+
export function compareDistVersion(opts) {
|
|
78
|
+
const distReader = opts.distVersionReader ?? defaultDistVersionReader;
|
|
79
|
+
const sourceReader = opts.sourceVersionReader ?? defaultSourceVersionReader;
|
|
80
|
+
const dist = safeRead(() => distReader(opts.projectRoot));
|
|
81
|
+
const source = safeRead(() => sourceReader(opts.projectRoot)) ?? 'unknown';
|
|
82
|
+
const distReadable = dist !== null;
|
|
83
|
+
return {
|
|
84
|
+
dist,
|
|
85
|
+
source,
|
|
86
|
+
match: distReadable && dist === source,
|
|
87
|
+
distReadable
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function safeRead(reader) {
|
|
91
|
+
try {
|
|
92
|
+
return reader();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function defaultDistVersionReader(projectRoot) {
|
|
99
|
+
// Synchronous read is fine: the dist version.js is small and on the
|
|
100
|
+
// local build pipeline's hot path. readFileSync + regex is cheaper
|
|
101
|
+
// than pulling in fs/promises for a single short file.
|
|
102
|
+
const distPath = join(projectRoot, 'dist', 'src', 'shared', 'version.js');
|
|
103
|
+
if (!existsSync(distPath)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const body = readFileSync(distPath, 'utf8');
|
|
107
|
+
const match = /export\s+const\s+CLI_VERSION\s*=\s*["']([^"']+)["']/.exec(body);
|
|
108
|
+
return match?.[1] ?? null;
|
|
109
|
+
}
|
|
110
|
+
function defaultSourceVersionReader(projectRoot) {
|
|
111
|
+
const pkgPath = join(projectRoot, 'package.json');
|
|
112
|
+
if (!existsSync(pkgPath)) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const body = readFileSync(pkgPath, 'utf8');
|
|
116
|
+
const parsed = JSON.parse(body);
|
|
117
|
+
return typeof parsed.version === 'string' ? parsed.version : null;
|
|
118
|
+
}
|
|
119
|
+
function defaultDistVersionProbe() {
|
|
120
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
121
|
+
if (projectRoot === null) {
|
|
122
|
+
return { dist: null, source: 'unknown', match: false, distReadable: false };
|
|
123
|
+
}
|
|
124
|
+
return compareDistVersion({ projectRoot });
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Pure helper that inspects the on-disk workspace layout for
|
|
128
|
+
* post-F3-canonical violations. The post-F3 canonical layout puts
|
|
129
|
+
* session dirs under `.peaks/_runtime/<sid>/` and the runtime
|
|
130
|
+
* binding at `.peaks/_runtime/session.json`; the legacy paths
|
|
131
|
+
* (top-level `<YYYY-MM-DD-session-<hex>>/` dirs and the legacy
|
|
132
|
+
* top-level `.peaks/.session.json` / `.peaks/.active-skill.json`
|
|
133
|
+
* dotfiles) must be absent. This helper is exported so tests can
|
|
134
|
+
* drive the filesystem walk without monkey-patching `process.cwd()`
|
|
135
|
+
* or `findProjectRoot`.
|
|
136
|
+
*
|
|
137
|
+
* Both scanners fail-soft (return `[]` on read errors) so a flaky
|
|
138
|
+
* filesystem read on a non-fatal probe path never escalates into a
|
|
139
|
+
* doctor failure.
|
|
140
|
+
*/
|
|
141
|
+
export function inspectWorkspaceLayout(opts) {
|
|
142
|
+
const topLevel = opts.topLevelScanner ?? defaultTopLevelSessionDirScanner;
|
|
143
|
+
const dotfiles = opts.dotfileScanner ?? defaultLegacyDotfileScanner;
|
|
144
|
+
const perChangeId = opts.perChangeIdScanner ?? defaultPerChangeIdDirScanner;
|
|
145
|
+
return {
|
|
146
|
+
topLevelSessionDirs: safeList(() => topLevel(opts.projectRoot)),
|
|
147
|
+
legacyDotfiles: safeList(() => dotfiles(opts.projectRoot)),
|
|
148
|
+
perChangeIdDirs: safeList(() => perChangeId(opts.projectRoot))
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function safeList(reader) {
|
|
152
|
+
try {
|
|
153
|
+
const out = reader();
|
|
154
|
+
return Array.isArray(out) ? out : [];
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const SESSION_DIR_PATTERN = /^\d{4}-\d{2}-\d{2}-session-[a-f0-9]+$/;
|
|
161
|
+
function defaultTopLevelSessionDirScanner(projectRoot) {
|
|
162
|
+
const peaksRoot = join(projectRoot, '.peaks');
|
|
163
|
+
if (!existsSync(peaksRoot))
|
|
164
|
+
return [];
|
|
165
|
+
let names;
|
|
166
|
+
try {
|
|
167
|
+
names = readdirSync(peaksRoot);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
const offenders = [];
|
|
173
|
+
for (const name of names) {
|
|
174
|
+
if (!SESSION_DIR_PATTERN.test(name))
|
|
175
|
+
continue;
|
|
176
|
+
const full = join(peaksRoot, name);
|
|
177
|
+
try {
|
|
178
|
+
const stat = existsSync(full) ? lstatSync(full) : null;
|
|
179
|
+
if (stat === null)
|
|
180
|
+
continue;
|
|
181
|
+
// Directories only — the regex should never match a dotfile or
|
|
182
|
+
// regular file, but be defensive against weird filesystem state
|
|
183
|
+
// (e.g. someone manually created a file whose name happens to
|
|
184
|
+
// match the session-id pattern).
|
|
185
|
+
if (stat.isDirectory()) {
|
|
186
|
+
offenders.push(join('.peaks', name) + '/');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return offenders;
|
|
194
|
+
}
|
|
195
|
+
const LEGACY_DOTFILES = ['.session.json', '.active-skill.json'];
|
|
196
|
+
function defaultLegacyDotfileScanner(projectRoot) {
|
|
197
|
+
const peaksRoot = join(projectRoot, '.peaks');
|
|
198
|
+
if (!existsSync(peaksRoot))
|
|
199
|
+
return [];
|
|
200
|
+
const offenders = [];
|
|
201
|
+
for (const name of LEGACY_DOTFILES) {
|
|
202
|
+
if (existsSync(join(peaksRoot, name))) {
|
|
203
|
+
offenders.push(join('.peaks', name));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return offenders;
|
|
207
|
+
}
|
|
208
|
+
function defaultWorkspaceLayoutProbe() {
|
|
209
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
210
|
+
if (projectRoot === null) {
|
|
211
|
+
return { topLevelSessionDirs: [], legacyDotfiles: [], perChangeIdDirs: [] };
|
|
212
|
+
}
|
|
213
|
+
return inspectWorkspaceLayout({ projectRoot });
|
|
214
|
+
}
|
|
215
|
+
// Slice 007 — per-change-id top-level dir pattern. Matches the
|
|
216
|
+
// F3-canonical (pre-canonicalization) layout the 5 already-shipped
|
|
217
|
+
// slices left behind, e.g. `.peaks/001-2026-06-06-doctor-dist-version-check/`.
|
|
218
|
+
// The pattern is intentionally narrow so it does NOT match the
|
|
219
|
+
// post-F3 system dirs (`_runtime/`, `_dogfood/`, `retrospective/`,
|
|
220
|
+
// `issues/`, `memory/`, `perf-baseline/`, `project-scan/`, `sops/`,
|
|
221
|
+
// `0NN-session-...`, `YYYY-MM-DD-session-...`).
|
|
222
|
+
const PER_CHANGE_ID_PATTERN = /^\d{3}-\d{4}-\d{2}-\d{2}-[a-z][a-z0-9-]*[a-z0-9]$/;
|
|
223
|
+
function defaultPerChangeIdDirScanner(projectRoot) {
|
|
224
|
+
const peaksRoot = join(projectRoot, '.peaks');
|
|
225
|
+
if (!existsSync(peaksRoot))
|
|
226
|
+
return [];
|
|
227
|
+
let names;
|
|
228
|
+
try {
|
|
229
|
+
names = readdirSync(peaksRoot);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
const offenders = [];
|
|
235
|
+
for (const name of names) {
|
|
236
|
+
if (!PER_CHANGE_ID_PATTERN.test(name))
|
|
237
|
+
continue;
|
|
238
|
+
const full = join(peaksRoot, name);
|
|
239
|
+
try {
|
|
240
|
+
const stat = existsSync(full) ? lstatSync(full) : null;
|
|
241
|
+
if (stat === null)
|
|
242
|
+
continue;
|
|
243
|
+
// Directories only — the regex should never match a dotfile
|
|
244
|
+
// or regular file, but be defensive against weird filesystem
|
|
245
|
+
// state (e.g. someone manually created a file whose name
|
|
246
|
+
// happens to match the per-change-id pattern).
|
|
247
|
+
if (stat.isDirectory()) {
|
|
248
|
+
offenders.push(join('.peaks', name) + '/');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return offenders;
|
|
256
|
+
}
|
|
71
257
|
const DESTRUCTIVE_APPLY_PATTERNS = [
|
|
72
258
|
/peaks\s+memory\s+sync[^\n]*--apply/,
|
|
73
259
|
/peaks\s+memory\s+extract[^\n]*--apply/,
|
|
@@ -344,6 +530,95 @@ export async function runDoctor(options = {}) {
|
|
|
344
530
|
message: `@colbymchenry/codegraph not resolvable: ${getErrorMessage(error)}`
|
|
345
531
|
});
|
|
346
532
|
}
|
|
533
|
+
// Build-hygiene check: the published `dist/` ships a different CLI_VERSION
|
|
534
|
+
// than the source-of-truth `src/shared/version.ts` / `package.json#version`
|
|
535
|
+
// whenever the user runs `npx peaks` or `node bin/peaks.js` after `pnpm
|
|
536
|
+
// install` but before `pnpm build`. This is the silent-stale-CLI failure
|
|
537
|
+
// mode reported in `.peaks/2026-06-05-session-fecddb/txt/dogfood-2026-06-04-05.md`
|
|
538
|
+
// (F1). A missing dist/ is treated as informational (fresh clone, not broken)
|
|
539
|
+
// so the check does not flip the summary to red on a clean checkout.
|
|
540
|
+
const distProbe = options.distVersionProbe ?? defaultDistVersionProbe;
|
|
541
|
+
try {
|
|
542
|
+
const result = distProbe();
|
|
543
|
+
if (!result.distReadable) {
|
|
544
|
+
checks.push({
|
|
545
|
+
id: 'build:dist-version-matches-source',
|
|
546
|
+
ok: true,
|
|
547
|
+
message: `dist/ is not present; run \`pnpm build\` to populate dist/src/shared/version.js (source version ${result.source})`
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
else if (result.match) {
|
|
551
|
+
checks.push({
|
|
552
|
+
id: 'build:dist-version-matches-source',
|
|
553
|
+
ok: true,
|
|
554
|
+
message: `dist/src/shared/version.js ships CLI_VERSION ${result.dist} matching source ${result.source}`
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
checks.push({
|
|
559
|
+
id: 'build:dist-version-matches-source',
|
|
560
|
+
ok: false,
|
|
561
|
+
message: `dist/src/shared/version.js ships CLI_VERSION ${result.dist} but source ${result.source} is in src/shared/version.ts; run \`pnpm build\` to refresh dist/`
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
checks.push({
|
|
567
|
+
id: 'build:dist-version-matches-source',
|
|
568
|
+
ok: false,
|
|
569
|
+
message: `dist version check failed: ${getErrorMessage(error)}`
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
// Build-hygiene check: a non-canonical post-F3 workspace layout is
|
|
573
|
+
// the silent-regression failure mode that slice 003 explicitly chose
|
|
574
|
+
// to allow (the current session binding was kept at top-level as a
|
|
575
|
+
// safety measure). This check surfaces any leftover top-level
|
|
576
|
+
// session dirs, the legacy runtime dotfiles (`.peaks/.session.json`,
|
|
577
|
+
// `.peaks/.active-skill.json`), OR per-change-id top-level dirs
|
|
578
|
+
// (`.peaks/NNN-YYYY-MM-DD-<slug>/`) so a future contributor who
|
|
579
|
+
// manually recreates one of them is warned. The check is read-only;
|
|
580
|
+
// the fix path is `peaks workspace migrate --to-runtime --project
|
|
581
|
+
// <repo> --apply`.
|
|
582
|
+
//
|
|
583
|
+
// Slice 007 broadened this check to flag per-change-id top-level
|
|
584
|
+
// dirs (the 5 already-shipped slices left them behind). Until
|
|
585
|
+
// slice 008's migration consolidates them, the check reports
|
|
586
|
+
// `ok: false` for any repo that still has the legacy per-change-id
|
|
587
|
+
// layout.
|
|
588
|
+
const layoutProbe = options.workspaceLayoutProbe ?? defaultWorkspaceLayoutProbe;
|
|
589
|
+
try {
|
|
590
|
+
const layout = layoutProbe();
|
|
591
|
+
// Back-compat: probes injected by older tests (pre-slice-007)
|
|
592
|
+
// return a 2-field shape (no perChangeIdDirs). Treat missing
|
|
593
|
+
// field as empty.
|
|
594
|
+
const perChangeIdDirs = layout.perChangeIdDirs ?? [];
|
|
595
|
+
if (layout.topLevelSessionDirs.length === 0 && layout.legacyDotfiles.length === 0 && perChangeIdDirs.length === 0) {
|
|
596
|
+
checks.push({
|
|
597
|
+
id: 'build:workspace-layout-canonical',
|
|
598
|
+
ok: true,
|
|
599
|
+
message: 'Workspace layout is canonical: no top-level session dirs, no legacy runtime dotfiles, no per-change-id top-level dirs'
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
const offenders = [
|
|
604
|
+
...layout.topLevelSessionDirs.map((p) => `top-level session dir: ${p}`),
|
|
605
|
+
...layout.legacyDotfiles.map((p) => `legacy dotfile: ${p}`),
|
|
606
|
+
...perChangeIdDirs.map((p) => `per-change-id top-level dir: ${p}`)
|
|
607
|
+
];
|
|
608
|
+
checks.push({
|
|
609
|
+
id: 'build:workspace-layout-canonical',
|
|
610
|
+
ok: false,
|
|
611
|
+
message: `Workspace layout is not canonical. Offenders: ${offenders.join('; ')}. Run \`peaks workspace migrate --to-runtime --project <repo> --apply\` to consolidate.`
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
catch (error) {
|
|
616
|
+
checks.push({
|
|
617
|
+
id: 'build:workspace-layout-canonical',
|
|
618
|
+
ok: false,
|
|
619
|
+
message: `Workspace layout check failed: ${getErrorMessage(error)}`
|
|
620
|
+
});
|
|
621
|
+
}
|
|
347
622
|
try {
|
|
348
623
|
const schemaText = await readText(join(schemaRoot, 'doctor-report.schema.json'));
|
|
349
624
|
const schema = JSON.parse(schemaText);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IdeAdapter } from '../ide-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code adapter —— peaks-cli 的"起源 IDE"。
|
|
4
|
+
*
|
|
5
|
+
* 该 adapter 从原 `src/services/skills/hooks-settings-service.ts` 提取,保持
|
|
6
|
+
* 字节级兼容:用户在 Claude Code 环境下跑 `peaks hooks install` 产出的
|
|
7
|
+
* `.claude/settings.json` 与 refactor 前逐字节相同。
|
|
8
|
+
*
|
|
9
|
+
* 字段解释(见 PRD AC-1):
|
|
10
|
+
* - dirName = '.claude' : Claude Code 项目根下的 settings 目录
|
|
11
|
+
* - settingsFileName = 'settings.json'
|
|
12
|
+
* - envVar = 'CLAUDE_PROJECT_DIR' : Claude Code 注入的 env 变量,用于 ${...} 占位
|
|
13
|
+
* - hookEvent = 'PreToolUse' : Claude Code hook 数组 key
|
|
14
|
+
* - toolMatcher = 'Bash' | 'Task' : PreToolUse 数组元素的 matcher 字段
|
|
15
|
+
*
|
|
16
|
+
* 不可消除的 per-IDE 字段(见 tech-doc.md §1.3)。
|
|
17
|
+
*/
|
|
18
|
+
export declare const CLAUDE_CODE_ADAPTER: IdeAdapter;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { claudeCodeSubAgentDispatcher } from '../../dispatch/sub-agent-dispatcher.js';
|
|
4
|
+
/**
|
|
5
|
+
* Claude Code adapter —— peaks-cli 的"起源 IDE"。
|
|
6
|
+
*
|
|
7
|
+
* 该 adapter 从原 `src/services/skills/hooks-settings-service.ts` 提取,保持
|
|
8
|
+
* 字节级兼容:用户在 Claude Code 环境下跑 `peaks hooks install` 产出的
|
|
9
|
+
* `.claude/settings.json` 与 refactor 前逐字节相同。
|
|
10
|
+
*
|
|
11
|
+
* 字段解释(见 PRD AC-1):
|
|
12
|
+
* - dirName = '.claude' : Claude Code 项目根下的 settings 目录
|
|
13
|
+
* - settingsFileName = 'settings.json'
|
|
14
|
+
* - envVar = 'CLAUDE_PROJECT_DIR' : Claude Code 注入的 env 变量,用于 ${...} 占位
|
|
15
|
+
* - hookEvent = 'PreToolUse' : Claude Code hook 数组 key
|
|
16
|
+
* - toolMatcher = 'Bash' | 'Task' : PreToolUse 数组元素的 matcher 字段
|
|
17
|
+
*
|
|
18
|
+
* 不可消除的 per-IDE 字段(见 tech-doc.md §1.3)。
|
|
19
|
+
*/
|
|
20
|
+
export const CLAUDE_CODE_ADAPTER = {
|
|
21
|
+
id: 'claude-code',
|
|
22
|
+
displayName: 'Claude Code',
|
|
23
|
+
settings: {
|
|
24
|
+
dirName: '.claude',
|
|
25
|
+
settingsFileName: 'settings.json',
|
|
26
|
+
resolveSettingsFile: (scope, projectRoot) => {
|
|
27
|
+
const root = scope === 'global' ? homedir() : resolve(projectRoot ?? homedir());
|
|
28
|
+
return join(root, '.claude', 'settings.json');
|
|
29
|
+
},
|
|
30
|
+
supportsScope: () => true,
|
|
31
|
+
},
|
|
32
|
+
envVar: 'CLAUDE_PROJECT_DIR',
|
|
33
|
+
hookEvent: 'PreToolUse',
|
|
34
|
+
toolMatcher: 'Bash',
|
|
35
|
+
subAgentToolMatcher: 'Task',
|
|
36
|
+
// Slice #009: Claude Code uses the `Task` tool for sub-agent dispatch.
|
|
37
|
+
// The CLI calls `claudeCodeSubAgentDispatcher.buildToolCall` to construct
|
|
38
|
+
// the exact args shape the `Task` tool expects.
|
|
39
|
+
subAgentDispatcher: claudeCodeSubAgentDispatcher,
|
|
40
|
+
// Slice #010 G9: Claude Code supports the PreToolUse hook event in a
|
|
41
|
+
// form that can wrap `peaks sub-agent-dispatch-guard` as a sub-command.
|
|
42
|
+
// Opt in to the G9 hook install.
|
|
43
|
+
promptSizeAware: true,
|
|
44
|
+
installHints: [
|
|
45
|
+
'Restart Claude Code (or reload the window) so the PreToolUse hooks take effect.'
|
|
46
|
+
],
|
|
47
|
+
capabilities: {
|
|
48
|
+
gateEnforce: true,
|
|
49
|
+
progressStart: true,
|
|
50
|
+
statusline: true,
|
|
51
|
+
mcpInstall: true,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { IdeAdapter } from '../ide-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Trae IDE adapter —— peaks-cli 的第二个内置 IDE 适配器。
|
|
4
|
+
*
|
|
5
|
+
* 不可消除的 per-IDE 字段(slice #1 锁定):
|
|
6
|
+
* - settings.dirName = '.trae' : Trae 项目根下的配置目录
|
|
7
|
+
* - settings.settingsFileName = 'settings.json' (UNVERIFIED at slice time: Trae 实际叫什么待 Trae 1.x 文档确认,先按 Claude 风格)
|
|
8
|
+
* - envVar = 'TRAE_PROJECT_DIR' : Trae 注入的 env 变量(用于 ${...} 占位)
|
|
9
|
+
* - hookEvent = 'beforeToolCall' : UNVERIFIED — Trae 的 hook 数组 key(待 Trae 文档确认,先假设与 Cursor 同名)
|
|
10
|
+
* - toolMatcher = 'terminal' : UNVERIFIED — Trae 的 bash 工具 matcher(待 Trae 文档确认)
|
|
11
|
+
*
|
|
12
|
+
* Slice #1 的 slim `IdeAdapter` shape 在 slice #1 RD 中被锁为"填表"模式。
|
|
13
|
+
* 本文件是 slice #2 第一个真实客户,验证 slice #1 抽出的形状真的可以
|
|
14
|
+
* 简单复制粘贴就接入新 IDE。
|
|
15
|
+
*
|
|
16
|
+
* 与 slice #1 claude-code-adapter.ts 的区别(故意):
|
|
17
|
+
* - Trae 的 hookEvent 名是 `beforeToolCall` 而不是 `PreToolUse`(假设)
|
|
18
|
+
* - Trae 的 toolMatcher 是 `terminal` 而不是 `Bash`(假设)
|
|
19
|
+
* - Trae 的 settings 路径是 `.trae/settings.json`(同 Claude 风格,只是目录名不同)
|
|
20
|
+
* - Trae 的 envVar 是 `TRAE_PROJECT_DIR`
|
|
21
|
+
* - installHints 提示用户"重启 Trae"(同 Claude 风格)
|
|
22
|
+
*
|
|
23
|
+
* 等 Trae 真实文档/真实用户的 dogfood 之后,可能需要把 hookEvent /
|
|
24
|
+
* toolMatcher 替换为 Trae 实际值。slice #2 的 tech-doc 里要明确"此 adapter
|
|
25
|
+
* 是基于 1.x 假设,Trae 真实集成需要在 Trae 上 dogfood 验证"。
|
|
26
|
+
*
|
|
27
|
+
* Slice #3 refactor: the `peaks hooks install` command now dispatches on the
|
|
28
|
+
* IDE adapter (auto-detect from env / cwd, override with `--ide trae`). When
|
|
29
|
+
* a Trae install is run, the resulting `<root>/.trae/settings.json` will use
|
|
30
|
+
* the `beforeToolCall` event key and the `terminal` matcher from this adapter.
|
|
31
|
+
* Until a real Trae 1.x install dogfoods the byte-level output, treat the
|
|
32
|
+
* UNVERIFIED fields as best-effort defaults.
|
|
33
|
+
*/
|
|
34
|
+
export declare const TRAE_ADAPTER: IdeAdapter;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { traeSubAgentDispatcher } from '../../dispatch/sub-agent-dispatcher.js';
|
|
4
|
+
/**
|
|
5
|
+
* Trae IDE adapter —— peaks-cli 的第二个内置 IDE 适配器。
|
|
6
|
+
*
|
|
7
|
+
* 不可消除的 per-IDE 字段(slice #1 锁定):
|
|
8
|
+
* - settings.dirName = '.trae' : Trae 项目根下的配置目录
|
|
9
|
+
* - settings.settingsFileName = 'settings.json' (UNVERIFIED at slice time: Trae 实际叫什么待 Trae 1.x 文档确认,先按 Claude 风格)
|
|
10
|
+
* - envVar = 'TRAE_PROJECT_DIR' : Trae 注入的 env 变量(用于 ${...} 占位)
|
|
11
|
+
* - hookEvent = 'beforeToolCall' : UNVERIFIED — Trae 的 hook 数组 key(待 Trae 文档确认,先假设与 Cursor 同名)
|
|
12
|
+
* - toolMatcher = 'terminal' : UNVERIFIED — Trae 的 bash 工具 matcher(待 Trae 文档确认)
|
|
13
|
+
*
|
|
14
|
+
* Slice #1 的 slim `IdeAdapter` shape 在 slice #1 RD 中被锁为"填表"模式。
|
|
15
|
+
* 本文件是 slice #2 第一个真实客户,验证 slice #1 抽出的形状真的可以
|
|
16
|
+
* 简单复制粘贴就接入新 IDE。
|
|
17
|
+
*
|
|
18
|
+
* 与 slice #1 claude-code-adapter.ts 的区别(故意):
|
|
19
|
+
* - Trae 的 hookEvent 名是 `beforeToolCall` 而不是 `PreToolUse`(假设)
|
|
20
|
+
* - Trae 的 toolMatcher 是 `terminal` 而不是 `Bash`(假设)
|
|
21
|
+
* - Trae 的 settings 路径是 `.trae/settings.json`(同 Claude 风格,只是目录名不同)
|
|
22
|
+
* - Trae 的 envVar 是 `TRAE_PROJECT_DIR`
|
|
23
|
+
* - installHints 提示用户"重启 Trae"(同 Claude 风格)
|
|
24
|
+
*
|
|
25
|
+
* 等 Trae 真实文档/真实用户的 dogfood 之后,可能需要把 hookEvent /
|
|
26
|
+
* toolMatcher 替换为 Trae 实际值。slice #2 的 tech-doc 里要明确"此 adapter
|
|
27
|
+
* 是基于 1.x 假设,Trae 真实集成需要在 Trae 上 dogfood 验证"。
|
|
28
|
+
*
|
|
29
|
+
* Slice #3 refactor: the `peaks hooks install` command now dispatches on the
|
|
30
|
+
* IDE adapter (auto-detect from env / cwd, override with `--ide trae`). When
|
|
31
|
+
* a Trae install is run, the resulting `<root>/.trae/settings.json` will use
|
|
32
|
+
* the `beforeToolCall` event key and the `terminal` matcher from this adapter.
|
|
33
|
+
* Until a real Trae 1.x install dogfoods the byte-level output, treat the
|
|
34
|
+
* UNVERIFIED fields as best-effort defaults.
|
|
35
|
+
*/
|
|
36
|
+
export const TRAE_ADAPTER = {
|
|
37
|
+
id: 'trae',
|
|
38
|
+
displayName: 'Trae',
|
|
39
|
+
settings: {
|
|
40
|
+
dirName: '.trae',
|
|
41
|
+
settingsFileName: 'settings.json', // UNVERIFIED — see slice #2 closeout code-review M-1
|
|
42
|
+
resolveSettingsFile: (scope, projectRoot) => {
|
|
43
|
+
const root = scope === 'global' ? homedir() : resolve(projectRoot ?? homedir());
|
|
44
|
+
return join(root, '.trae', 'settings.json');
|
|
45
|
+
},
|
|
46
|
+
supportsScope: (scope) => scope === 'project' || scope === 'global'
|
|
47
|
+
},
|
|
48
|
+
envVar: 'TRAE_PROJECT_DIR',
|
|
49
|
+
hookEvent: 'beforeToolCall', // UNVERIFIED — see slice #2 closeout code-review M-1; will be validated when a real Trae 1.x install dogfoods the install path
|
|
50
|
+
toolMatcher: 'terminal', // UNVERIFIED — see slice #2 closeout code-review M-1
|
|
51
|
+
subAgentToolMatcher: 'Task', // UNVERIFIED — Trae's sub-agent tool name is unknown; matches the prior hardcoded 'Task' literal so byte-level install output is unchanged. Will be dogfooded when a real Trae 1.x install dispatches a sub-agent.
|
|
52
|
+
// Slice #009: Trae's sub-agent dispatcher is UNVERIFIED — Trae sub-agent
|
|
53
|
+
// tool name TBD on real dogfood; byte-level identical to claude-code by
|
|
54
|
+
// design so the slice #008 `subAgentToolMatcher: 'Task'` install entry
|
|
55
|
+
// stays byte-stable. Awaiting real Trae 1.x dogfood to confirm/replace.
|
|
56
|
+
subAgentDispatcher: traeSubAgentDispatcher,
|
|
57
|
+
// Slice #010 G9: Trae supports `beforeToolCall` which can wrap
|
|
58
|
+
// `peaks sub-agent-dispatch-guard`. Opt in (matches the byte-stable
|
|
59
|
+
// slice #008 install entry shape).
|
|
60
|
+
promptSizeAware: true,
|
|
61
|
+
installHints: [
|
|
62
|
+
'Restart Trae (or reload the workspace) so the beforeToolCall hooks take effect.'
|
|
63
|
+
],
|
|
64
|
+
capabilities: {
|
|
65
|
+
gateEnforce: true,
|
|
66
|
+
progressStart: true,
|
|
67
|
+
statusline: true,
|
|
68
|
+
mcpInstall: false // Trae 的 MCP 集成尚未确定,先关掉避免误导
|
|
69
|
+
}
|
|
70
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { PEAKS_HOOK_SCHEMA, type IdeId, type PeaksCanonicalHook, type PeaksDecisionTransport } from './ide-types.js';
|
|
2
|
+
export { PEAKS_HOOK_SCHEMA };
|
|
3
|
+
export type { PeaksCanonicalHook, PeaksDecisionTransport };
|
|
4
|
+
/**
|
|
5
|
+
* Compute the deny decision shape for Claude Code (the only adapter registered
|
|
6
|
+
* in slice #1). The output is a JSON object that, when written to stdout, makes
|
|
7
|
+
* the Claude Code permission system block the tool call BEFORE the user's
|
|
8
|
+
* permission prompt — un-bypassable, even under --dangerously-skip-permissions.
|
|
9
|
+
*/
|
|
10
|
+
export declare const CLAUDE_CODE_DENY_SHAPE: Record<string, unknown>;
|
|
11
|
+
export declare const CLAUDE_CODE_DENY_TRANSPORT: PeaksDecisionTransport;
|
|
12
|
+
/**
|
|
13
|
+
* Compute the deny decision shape for Trae (Cursor-style sibling IDE).
|
|
14
|
+
* UNVERIFIED — Trae 1.x's actual response envelope is a 1.x assumption
|
|
15
|
+
* (see src/services/ide/adapters/trae-adapter.ts). Slice #3 ships a
|
|
16
|
+
* Cursor-style envelope as the best-effort default; if a future slice
|
|
17
|
+
* confirms Trae's actual shape, update this constant and the related test.
|
|
18
|
+
*/
|
|
19
|
+
export declare const TRAE_DENY_SHAPE: Record<string, unknown>;
|
|
20
|
+
export declare const TRAE_DENY_TRANSPORT: PeaksDecisionTransport;
|
|
21
|
+
/**
|
|
22
|
+
* Format a decision response for a given IDE. Slice #1 handles Claude Code;
|
|
23
|
+
* slice #3 added Trae (1.x-assumption shape — see TRAE_DENY_SHAPE doc).
|
|
24
|
+
* Future slices will add exit-code / both variants for IDEs that don't read
|
|
25
|
+
* stdout.
|
|
26
|
+
*/
|
|
27
|
+
export declare function formatDecisionResponse(ide: IdeId, decision: 'allow' | 'deny', reason?: string): {
|
|
28
|
+
stdout: string;
|
|
29
|
+
exitCode: number;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Build a peaks canonical hook from a parsed stdin payload. Caller has already
|
|
33
|
+
* done stdin parsing + IDE auto-detection; this function normalizes to the
|
|
34
|
+
* canonical schema.
|
|
35
|
+
*/
|
|
36
|
+
export interface BuildCanonicalHookInput {
|
|
37
|
+
readonly toolName: string;
|
|
38
|
+
readonly toolInput: Record<string, unknown>;
|
|
39
|
+
readonly projectRoot: string;
|
|
40
|
+
readonly rawIdeFormat: IdeId;
|
|
41
|
+
readonly rawPayload: unknown;
|
|
42
|
+
readonly event?: PeaksCanonicalHook['event'];
|
|
43
|
+
}
|
|
44
|
+
export declare function buildCanonicalHook(input: BuildCanonicalHookInput): PeaksCanonicalHook;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { PEAKS_HOOK_SCHEMA } from './ide-types.js';
|
|
2
|
+
export { PEAKS_HOOK_SCHEMA };
|
|
3
|
+
/**
|
|
4
|
+
* Compute the deny decision shape for Claude Code (the only adapter registered
|
|
5
|
+
* in slice #1). The output is a JSON object that, when written to stdout, makes
|
|
6
|
+
* the Claude Code permission system block the tool call BEFORE the user's
|
|
7
|
+
* permission prompt — un-bypassable, even under --dangerously-skip-permissions.
|
|
8
|
+
*/
|
|
9
|
+
export const CLAUDE_CODE_DENY_SHAPE = {
|
|
10
|
+
hookSpecificOutput: {
|
|
11
|
+
hookEventName: 'PreToolUse',
|
|
12
|
+
permissionDecision: 'deny',
|
|
13
|
+
permissionDecisionReason: '__REASON__' // replaced at format time
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
export const CLAUDE_CODE_DENY_TRANSPORT = {
|
|
17
|
+
kind: 'stdout-json',
|
|
18
|
+
denyShape: CLAUDE_CODE_DENY_SHAPE
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Compute the deny decision shape for Trae (Cursor-style sibling IDE).
|
|
22
|
+
* UNVERIFIED — Trae 1.x's actual response envelope is a 1.x assumption
|
|
23
|
+
* (see src/services/ide/adapters/trae-adapter.ts). Slice #3 ships a
|
|
24
|
+
* Cursor-style envelope as the best-effort default; if a future slice
|
|
25
|
+
* confirms Trae's actual shape, update this constant and the related test.
|
|
26
|
+
*/
|
|
27
|
+
export const TRAE_DENY_SHAPE = {
|
|
28
|
+
hookSpecificOutput: {
|
|
29
|
+
hookEventName: 'beforeToolCall',
|
|
30
|
+
permissionDecision: 'deny',
|
|
31
|
+
permissionDecisionReason: '__REASON__' // replaced at format time
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
export const TRAE_DENY_TRANSPORT = {
|
|
35
|
+
kind: 'stdout-json',
|
|
36
|
+
denyShape: TRAE_DENY_SHAPE
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Format a decision response for a given IDE. Slice #1 handles Claude Code;
|
|
40
|
+
* slice #3 added Trae (1.x-assumption shape — see TRAE_DENY_SHAPE doc).
|
|
41
|
+
* Future slices will add exit-code / both variants for IDEs that don't read
|
|
42
|
+
* stdout.
|
|
43
|
+
*/
|
|
44
|
+
export function formatDecisionResponse(ide, decision, reason) {
|
|
45
|
+
if (decision === 'allow') {
|
|
46
|
+
return { stdout: '', exitCode: 0 };
|
|
47
|
+
}
|
|
48
|
+
let shape;
|
|
49
|
+
if (ide === 'claude-code') {
|
|
50
|
+
shape = CLAUDE_CODE_DENY_SHAPE;
|
|
51
|
+
}
|
|
52
|
+
else if (ide === 'trae') {
|
|
53
|
+
shape = TRAE_DENY_SHAPE;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
throw new Error(`formatDecisionResponse: unsupported IDE ${ide} (not registered in adapter registry; future slice will add support)`);
|
|
57
|
+
}
|
|
58
|
+
const filled = JSON.stringify(shape).replace('"__REASON__"', JSON.stringify(reason ?? 'denied'));
|
|
59
|
+
return { stdout: filled, exitCode: 0 };
|
|
60
|
+
}
|
|
61
|
+
export function buildCanonicalHook(input) {
|
|
62
|
+
return {
|
|
63
|
+
schema: PEAKS_HOOK_SCHEMA,
|
|
64
|
+
event: input.event ?? 'pre-tool-use',
|
|
65
|
+
toolName: input.toolName,
|
|
66
|
+
toolInput: input.toolInput,
|
|
67
|
+
projectRoot: input.projectRoot,
|
|
68
|
+
rawIdeFormat: input.rawIdeFormat,
|
|
69
|
+
rawPayload: input.rawPayload
|
|
70
|
+
};
|
|
71
|
+
}
|