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.
Files changed (111) hide show
  1. package/README.md +6 -2
  2. package/bin/peaks.js +0 -0
  3. package/dist/src/cli/commands/core-artifact-commands.js +49 -11
  4. package/dist/src/cli/commands/gate-commands.js +28 -19
  5. package/dist/src/cli/commands/hook-handle.d.ts +17 -0
  6. package/dist/src/cli/commands/hook-handle.js +111 -0
  7. package/dist/src/cli/commands/hooks-commands.js +72 -21
  8. package/dist/src/cli/commands/progress-commands.js +9 -2
  9. package/dist/src/cli/commands/progress-start-spawn.js +30 -4
  10. package/dist/src/cli/commands/slice-commands.js +4 -2
  11. package/dist/src/cli/commands/statusline-commands.js +75 -17
  12. package/dist/src/cli/commands/sub-agent-commands.d.ts +5 -0
  13. package/dist/src/cli/commands/sub-agent-commands.js +488 -0
  14. package/dist/src/cli/commands/sub-agent-dispatch-guard.d.ts +55 -0
  15. package/dist/src/cli/commands/sub-agent-dispatch-guard.js +57 -0
  16. package/dist/src/cli/commands/workspace-commands.js +70 -14
  17. package/dist/src/cli/program.js +9 -0
  18. package/dist/src/hooks/pre-tool-use-sub-agent.d.ts +28 -0
  19. package/dist/src/hooks/pre-tool-use-sub-agent.js +105 -0
  20. package/dist/src/services/artifacts/artifact-prerequisites.d.ts +12 -0
  21. package/dist/src/services/artifacts/artifact-prerequisites.js +39 -8
  22. package/dist/src/services/artifacts/request-artifact-service.js +116 -76
  23. package/dist/src/services/config/config-types.d.ts +1 -1
  24. package/dist/src/services/context/artifact-meta.d.ts +72 -0
  25. package/dist/src/services/context/artifact-meta.js +105 -0
  26. package/dist/src/services/context/context-guard.d.ts +49 -0
  27. package/dist/src/services/context/context-guard.js +91 -0
  28. package/dist/src/services/context/dispatch-context-guard.d.ts +27 -0
  29. package/dist/src/services/context/dispatch-context-guard.js +192 -0
  30. package/dist/src/services/context/headroom-client.d.ts +34 -0
  31. package/dist/src/services/context/headroom-client.js +117 -0
  32. package/dist/src/services/context/shared-channel.d.ts +92 -0
  33. package/dist/src/services/context/shared-channel.js +285 -0
  34. package/dist/src/services/context/threshold.d.ts +35 -0
  35. package/dist/src/services/context/threshold.js +76 -0
  36. package/dist/src/services/dispatch/batch-counter.d.ts +27 -0
  37. package/dist/src/services/dispatch/batch-counter.js +85 -0
  38. package/dist/src/services/dispatch/dispatch-record-writer.d.ts +93 -0
  39. package/dist/src/services/dispatch/dispatch-record-writer.js +261 -0
  40. package/dist/src/services/dispatch/heartbeat-truncator.d.ts +26 -0
  41. package/dist/src/services/dispatch/heartbeat-truncator.js +13 -0
  42. package/dist/src/services/dispatch/leak-detector.d.ts +11 -0
  43. package/dist/src/services/dispatch/leak-detector.js +72 -0
  44. package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +127 -0
  45. package/dist/src/services/dispatch/sub-agent-dispatcher.js +98 -0
  46. package/dist/src/services/doctor/doctor-service.d.ts +62 -0
  47. package/dist/src/services/doctor/doctor-service.js +276 -1
  48. package/dist/src/services/ide/adapters/claude-code-adapter.d.ts +18 -0
  49. package/dist/src/services/ide/adapters/claude-code-adapter.js +53 -0
  50. package/dist/src/services/ide/adapters/trae-adapter.d.ts +34 -0
  51. package/dist/src/services/ide/adapters/trae-adapter.js +70 -0
  52. package/dist/src/services/ide/hook-protocol.d.ts +44 -0
  53. package/dist/src/services/ide/hook-protocol.js +71 -0
  54. package/dist/src/services/ide/hook-translator.d.ts +72 -0
  55. package/dist/src/services/ide/hook-translator.js +128 -0
  56. package/dist/src/services/ide/ide-detector.d.ts +10 -0
  57. package/dist/src/services/ide/ide-detector.js +19 -0
  58. package/dist/src/services/ide/ide-registry.d.ts +14 -0
  59. package/dist/src/services/ide/ide-registry.js +45 -0
  60. package/dist/src/services/ide/ide-types.d.ts +120 -0
  61. package/dist/src/services/ide/ide-types.js +2 -0
  62. package/dist/src/services/ide/shared/atomic-json.d.ts +15 -0
  63. package/dist/src/services/ide/shared/atomic-json.js +58 -0
  64. package/dist/src/services/ide/shared/safe-path.d.ts +11 -0
  65. package/dist/src/services/ide/shared/safe-path.js +29 -0
  66. package/dist/src/services/progress/progress-service.d.ts +1 -1
  67. package/dist/src/services/progress/progress-service.js +18 -14
  68. package/dist/src/services/security/safe-settings-path.d.ts +12 -0
  69. package/dist/src/services/security/safe-settings-path.js +104 -0
  70. package/dist/src/services/session/session-manager.d.ts +22 -1
  71. package/dist/src/services/session/session-manager.js +137 -28
  72. package/dist/src/services/signal/cancel-handler.d.ts +14 -0
  73. package/dist/src/services/signal/cancel-handler.js +76 -0
  74. package/dist/src/services/skill/resume-detector.d.ts +54 -0
  75. package/dist/src/services/skill/resume-detector.js +334 -0
  76. package/dist/src/services/skill/skill-scheduler.d.ts +40 -0
  77. package/dist/src/services/skill/skill-scheduler.js +53 -0
  78. package/dist/src/services/skills/hooks-settings-service.d.ts +47 -29
  79. package/dist/src/services/skills/hooks-settings-service.js +190 -144
  80. package/dist/src/services/skills/statusline-settings-service.d.ts +33 -6
  81. package/dist/src/services/skills/statusline-settings-service.js +31 -34
  82. package/dist/src/services/slice/slice-archive-service.d.ts +20 -0
  83. package/dist/src/services/slice/slice-archive-service.js +111 -0
  84. package/dist/src/services/slice/slice-check-service.js +20 -1
  85. package/dist/src/services/slice/slice-check-types.d.ts +9 -0
  86. package/dist/src/services/solo/batch-heartbeat-poller.d.ts +51 -0
  87. package/dist/src/services/solo/batch-heartbeat-poller.js +88 -0
  88. package/dist/src/services/solo/status-line-renderer.d.ts +34 -0
  89. package/dist/src/services/solo/status-line-renderer.js +55 -0
  90. package/dist/src/services/workspace/migrate-service.js +124 -2
  91. package/dist/src/services/workspace/migrate-types.d.ts +50 -7
  92. package/dist/src/services/workspace/reconcile-service.d.ts +69 -0
  93. package/dist/src/services/workspace/reconcile-service.js +267 -48
  94. package/dist/src/services/workspace/reconcile-types.d.ts +37 -0
  95. package/dist/src/services/workspace/workspace-service.js +29 -62
  96. package/dist/src/shared/version.d.ts +1 -1
  97. package/dist/src/shared/version.js +1 -1
  98. package/package.json +2 -1
  99. package/schemas/doctor-report.schema.json +2 -2
  100. package/skills/peaks-ide/SKILL.md +159 -0
  101. package/skills/peaks-qa/SKILL.md +58 -1
  102. package/skills/peaks-qa/references/qa-fanout-contract.md +150 -0
  103. package/skills/peaks-rd/SKILL.md +52 -9
  104. package/skills/peaks-solo/SKILL.md +83 -20
  105. package/skills/peaks-solo/references/context-governance.md +144 -0
  106. package/skills/peaks-solo/references/headroom-integration.md +107 -0
  107. package/skills/peaks-solo/references/runbook.md +3 -3
  108. package/skills/peaks-solo/references/sub-agent-dispatch.md +218 -0
  109. package/skills/peaks-solo/references/swarm-dispatch-contract.md +3 -37
  110. package/skills/peaks-txt/SKILL.md +19 -0
  111. 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
+ }