peaks-cli 1.3.3 → 1.3.5

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 (61) hide show
  1. package/dist/src/cli/commands/core-artifact-commands.js +6 -3
  2. package/dist/src/cli/commands/hook-handle.d.ts +2 -2
  3. package/dist/src/cli/commands/hook-handle.js +5 -10
  4. package/dist/src/cli/commands/hooks-commands.js +44 -29
  5. package/dist/src/cli/commands/project-commands.js +15 -5
  6. package/dist/src/cli/commands/workflow-commands.js +2 -1
  7. package/dist/src/cli/commands/workspace-commands.js +1 -2
  8. package/dist/src/cli/program.js +3 -2
  9. package/dist/src/services/dashboard/project-dashboard-service.d.ts +23 -0
  10. package/dist/src/services/dashboard/project-dashboard-service.js +21 -0
  11. package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +45 -40
  12. package/dist/src/services/dispatch/sub-agent-dispatcher.js +25 -20
  13. package/dist/src/services/ide/adapters/claude-code-adapter.js +27 -2
  14. package/dist/src/services/ide/adapters/trae-adapter.d.ts +19 -11
  15. package/dist/src/services/ide/adapters/trae-adapter.js +45 -19
  16. package/dist/src/services/ide/hook-protocol.d.ts +7 -4
  17. package/dist/src/services/ide/hook-protocol.js +7 -4
  18. package/dist/src/services/ide/ide-types.d.ts +61 -16
  19. package/dist/src/services/ide/resource-profile.d.ts +52 -0
  20. package/dist/src/services/ide/resource-profile.js +33 -0
  21. package/dist/src/services/memory/project-context-service.js +2 -1
  22. package/dist/src/services/memory/project-memory-service.js +4 -3
  23. package/dist/src/services/perf/perf-baseline-service.js +2 -1
  24. package/dist/src/services/progress/progress-service.d.ts +23 -103
  25. package/dist/src/services/progress/progress-service.js +24 -137
  26. package/dist/src/services/scan/file-size-scan.d.ts +4 -0
  27. package/dist/src/services/scan/file-size-scan.js +32 -3
  28. package/dist/src/services/session/getSessionDir.d.ts +1 -0
  29. package/dist/src/services/session/getSessionDir.js +27 -0
  30. package/dist/src/services/session/index.d.ts +1 -0
  31. package/dist/src/services/session/index.js +1 -0
  32. package/dist/src/services/skills/hooks-settings-service.d.ts +57 -5
  33. package/dist/src/services/skills/hooks-settings-service.js +153 -28
  34. package/dist/src/services/standards/ide-aware-standards-service.d.ts +94 -0
  35. package/dist/src/services/standards/ide-aware-standards-service.js +89 -0
  36. package/dist/src/services/standards/project-standards-service.d.ts +1 -2
  37. package/dist/src/shared/incrementing-number.d.ts +0 -8
  38. package/dist/src/shared/incrementing-number.js +11 -1
  39. package/dist/src/shared/version.d.ts +1 -1
  40. package/dist/src/shared/version.js +1 -1
  41. package/package.json +1 -1
  42. package/scripts/install-skills.mjs +112 -2
  43. package/skills/peaks-ide/SKILL.md +1 -1
  44. package/skills/peaks-ide/references/audit-log-helper.md +52 -0
  45. package/skills/peaks-qa/SKILL.md +104 -62
  46. package/skills/peaks-qa/references/qa-fanout-contract.md +6 -6
  47. package/skills/peaks-rd/SKILL.md +88 -73
  48. package/skills/peaks-solo/SKILL.md +52 -22
  49. package/skills/peaks-solo/references/browser-workflow.md +22 -20
  50. package/skills/peaks-solo/references/runbook.md +21 -21
  51. package/skills/peaks-solo/references/sub-agent-dispatch.md +44 -1
  52. package/skills/peaks-solo/references/swarm-dispatch-contract.md +9 -9
  53. package/skills/peaks-ui/SKILL.md +18 -9
  54. package/dist/src/cli/commands/progress-close-kill.d.ts +0 -51
  55. package/dist/src/cli/commands/progress-close-kill.js +0 -152
  56. package/dist/src/cli/commands/progress-commands.d.ts +0 -3
  57. package/dist/src/cli/commands/progress-commands.js +0 -379
  58. package/dist/src/cli/commands/progress-start-spawn.d.ts +0 -59
  59. package/dist/src/cli/commands/progress-start-spawn.js +0 -140
  60. package/dist/src/cli/commands/progress-watch-render.d.ts +0 -80
  61. package/dist/src/cli/commands/progress-watch-render.js +0 -308
@@ -10,36 +10,24 @@ import { getAdapter } from '../ide/ide-registry.js';
10
10
  // values are computed lazily inside each public function call.
11
11
  /** Sentinel substring identifying a Claude-Code gate-enforce hook entry. */
12
12
  export const HOOK_ENFORCE_SENTINEL = 'peaks gate enforce';
13
- /** Sentinel substring identifying a peaks-managed sub-agent-progress hook entry. */
14
- export const HOOK_PROGRESS_SENTINEL = 'peaks progress start';
15
13
  /** Default (claude-code) hook command — kept as a stable export for tests. */
16
14
  export const HOOK_ENFORCE_COMMAND = `peaks gate enforce --project "\${CLAUDE_PROJECT_DIR}"`;
17
- /** Default (claude-code) progress command — kept as a stable export for tests. */
18
- export const HOOK_PROGRESS_COMMAND = `peaks progress start --project "\${CLAUDE_PROJECT_DIR}" --reason "auto-spawn for sub-agent Task" --quiet`;
19
15
  function resolveHookSpec(ide) {
20
16
  const adapter = getAdapter(ide);
21
17
  if (ide === 'claude-code') {
22
18
  return {
23
19
  hookEnforceCommand: `peaks gate enforce --project "\${${adapter.envVar}}"`,
24
- hookProgressCommand: `peaks progress start --project "\${${adapter.envVar}}" --reason "auto-spawn for sub-agent ${adapter.subAgentToolMatcher}" --quiet`,
25
20
  hookEnforceSentinel: HOOK_ENFORCE_SENTINEL,
26
- hookProgressSentinel: HOOK_PROGRESS_SENTINEL,
27
21
  hookEnforceMatcher: adapter.toolMatcher, // 'Bash'
28
- hookProgressMatcher: adapter.subAgentToolMatcher, // 'Task' (slice 2026-06-06-sub-agent-spawn-bug-and-decouple — adapter now self-reports sub-agent tool name)
29
- hookEnforceEvent: adapter.hookEvent, // 'PreToolUse'
30
- hookProgressEvent: adapter.hookEvent // 'PreToolUse' for Claude
22
+ hookEnforceEvent: adapter.hookEvent // 'PreToolUse'
31
23
  };
32
24
  }
33
25
  if (ide === 'trae') {
34
26
  return {
35
27
  hookEnforceCommand: `peaks hook handle --project "\${${adapter.envVar}}"`,
36
- hookProgressCommand: `peaks progress start --project "\${${adapter.envVar}}" --reason "auto-spawn for sub-agent ${adapter.subAgentToolMatcher}" --quiet`,
37
28
  hookEnforceSentinel: 'peaks hook handle',
38
- hookProgressSentinel: HOOK_PROGRESS_SENTINEL,
39
29
  hookEnforceMatcher: adapter.toolMatcher, // 'terminal'
40
- hookProgressMatcher: adapter.subAgentToolMatcher, // 'Task' (UNVERIFIED for Trae; matches prior hardcoded literal so byte-level install output is unchanged)
41
- hookEnforceEvent: adapter.hookEvent, // 'beforeToolCall'
42
- hookProgressEvent: adapter.hookEvent // 'beforeToolCall' (no separate progress event yet for Trae)
30
+ hookEnforceEvent: adapter.hookEvent // 'beforeToolCall'
43
31
  };
44
32
  }
45
33
  // Future adapters (codex, cursor, qoder, tongyi-lingma) — not yet registered.
@@ -51,6 +39,10 @@ function resolveHookSpec(ide) {
51
39
  function resolveIde(options) {
52
40
  return options?.ide ?? 'claude-code';
53
41
  }
42
+ /** Slice #013: read the skipProgress opt-in flag. Slice #014: the underlying install no longer emits the progress-start entry, so the flag is effectively a no-op (kept for API stability). */
43
+ function resolveSkipProgress(options) {
44
+ return options?.skipProgress === true;
45
+ }
54
46
  /** Resolve settings root dir for a scope. */
55
47
  function resolveSettingsRoot(scope, projectRoot) {
56
48
  if (scope === 'global')
@@ -101,24 +93,104 @@ function entryIsPeaksManaged(entry, sentinels) {
101
93
  return sentinels.some((sentinel) => cmd.includes(sentinel));
102
94
  });
103
95
  }
96
+ /**
97
+ * Slice #014: read the *actually-installed* peaks-managed hook entries
98
+ * from a settings object. Replaces the pre-#014 `listInstalledEntriesForIde`
99
+ * helper in `hooks-commands.ts`, which returned the IDE-EXPECTED list
100
+ * (a hardcoded 2-entry array per adapter) rather than what was on disk.
101
+ * That bug surfaced when slice #013's local cleanup installed
102
+ * `peaks hooks install --no-progress` (gate-enforce only), but the
103
+ * status command still reported `entries: [Bash, Task]` because the
104
+ * helper didn't read the file.
105
+ *
106
+ * The new helper:
107
+ * 1. reads each `hooks.<event>` array,
108
+ * 2. filters to entries that are peaks-managed for the given IDE
109
+ * (matches the legacy sentinel set: gate-enforce + the no-longer-
110
+ * installed progress-start),
111
+ * 3. returns one `{ matcher, sentinel }` row per entry, taking the
112
+ * FIRST matching sentinel per entry (entries have a single command
113
+ * handler in practice, but the loop tolerates multi-handler
114
+ * entries by taking the first match).
115
+ *
116
+ * Pre-#014 settings.json files that have a stale progress-start entry
117
+ * will see it surface in the result. This is intentional: the status
118
+ * command is the user's tool for "what is on disk right now", and
119
+ * surfacing a stale entry is the only way the user can know to run
120
+ * `peaks hooks install` (which now strips it) or `peaks hooks
121
+ * uninstall` (which removes it).
122
+ */
123
+ export function readInstalledEntriesFromSettings(settings, ide) {
124
+ const sentinels = resolveLegacySentinels(ide);
125
+ // Walk every event key the settings file has, not just the
126
+ // adapter-declared one. A pre-#014 install could have left a
127
+ // progress-start entry on a different event than the gate-enforce
128
+ // entry (Trae: both on beforeToolCall, but a stale install on a
129
+ // future IDE could split them).
130
+ const hooksRoot = settings.hooks;
131
+ if (!hooksRoot || typeof hooksRoot !== 'object' || Array.isArray(hooksRoot))
132
+ return [];
133
+ const result = [];
134
+ for (const eventKey of Object.keys(hooksRoot)) {
135
+ const entries = readHookEntriesFromHooks(hooksRoot, eventKey);
136
+ for (const entry of entries) {
137
+ if (!entryIsPeaksManaged(entry, sentinels))
138
+ continue;
139
+ const matcher = typeof entry.matcher === 'string' ? entry.matcher : '';
140
+ // Find the first matching sentinel inside the entry's command
141
+ // handlers. For each handler, find the first sentinel substring
142
+ // it contains. We pick the first handler's first matching
143
+ // sentinel (entries have a single command in practice).
144
+ const firstHandler = Array.isArray(entry.hooks) ? entry.hooks[0] : undefined;
145
+ const cmd = typeof firstHandler?.command === 'string' ? firstHandler.command : '';
146
+ const sentinel = sentinels.find((s) => cmd.includes(s));
147
+ if (matcher === '' || sentinel === undefined)
148
+ continue;
149
+ result.push({ matcher, sentinel });
150
+ }
151
+ }
152
+ return result;
153
+ }
104
154
  /**
105
155
  * Compute the per-IDE peaks hook entries to merge into the settings file.
106
156
  * Replaces the slice #1 hardcoded `PEAKS_HOOK_ENTRIES` constant; the constant
107
157
  * remains exported (computed for claude-code) for backward compat.
158
+ *
159
+ * Slice #013 (`--no-progress` flag): when `skipProgress` is true, the
160
+ * progress-start entry is omitted from the returned list. Install with this
161
+ * flag will (a) NOT emit the progress hook entry, and (b) will idempotently
162
+ * remove any previously-installed progress entry (sentinel-based merge).
163
+ *
164
+ * Slice #014 (refactor — full removal): only the gate-enforce entry is
165
+ * ever emitted. The `skipProgress` parameter is kept for API stability
166
+ * but is a no-op — the returned list is always single-entry. The progress
167
+ * entry's sentinel is included in the legacy sentinel set so uninstall
168
+ * can find + remove any stale progress-start entry that an older
169
+ * `peaks hooks install` may have written before this slice.
108
170
  */
109
- function resolveHookEntries(ide) {
171
+ function resolveHookEntries(ide, _skipProgress = false) {
110
172
  const spec = resolveHookSpec(ide);
111
173
  return [
112
- { sentinel: spec.hookEnforceSentinel, matcher: spec.hookEnforceMatcher, command: spec.hookEnforceCommand, event: spec.hookEnforceEvent },
113
- { sentinel: spec.hookProgressSentinel, matcher: spec.hookProgressMatcher, command: spec.hookProgressCommand, event: spec.hookProgressEvent }
174
+ { sentinel: spec.hookEnforceSentinel, matcher: spec.hookEnforceMatcher, command: spec.hookEnforceCommand, event: spec.hookEnforceEvent }
114
175
  ];
115
176
  }
116
- /** Default (claude-code) peaks-managed hook entries — kept as a stable export for tests. */
177
+ /**
178
+ * Legacy sentinel set used by uninstall + status to find and remove stale
179
+ * progress-start entries written by pre-#014 installs. The progress-start
180
+ * sentinel is the literal substring that older installs emitted.
181
+ */
182
+ const LEGACY_PROGRESS_START_SENTINEL = 'peaks progress start';
183
+ function resolveLegacySentinels(ide) {
184
+ if (ide === 'trae') {
185
+ return ['peaks hook handle', LEGACY_PROGRESS_START_SENTINEL];
186
+ }
187
+ return [HOOK_ENFORCE_SENTINEL, LEGACY_PROGRESS_START_SENTINEL];
188
+ }
189
+ /** Default (claude-code) peaks-managed hook entries — kept as a stable export for tests. Slice #014: only the gate-enforce entry. */
117
190
  export const PEAKS_HOOK_ENTRIES = (() => {
118
191
  const spec = resolveHookSpec('claude-code');
119
192
  return [
120
- { sentinel: spec.hookEnforceSentinel, matcher: spec.hookEnforceMatcher, command: spec.hookEnforceCommand, event: spec.hookEnforceEvent },
121
- { sentinel: spec.hookProgressSentinel, matcher: spec.hookProgressMatcher, command: spec.hookProgressCommand, event: spec.hookProgressEvent }
193
+ { sentinel: spec.hookEnforceSentinel, matcher: spec.hookEnforceMatcher, command: spec.hookEnforceCommand, event: spec.hookEnforceEvent }
122
194
  ];
123
195
  })();
124
196
  function isInstalledForIde(settings, ide) {
@@ -133,8 +205,43 @@ function isInstalledForIde(settings, ide) {
133
205
  }
134
206
  return false;
135
207
  }
208
+ /**
209
+ * Slice #014: detect the "stale progress entry after pre-#014 install"
210
+ * case. The desired shape is gate-enforce-only. If the file currently
211
+ * has a peaks-managed progress-start entry (left behind by a pre-#014
212
+ * install), the install is NOT a no-op — it must strip the stale
213
+ * entry. This helper returns true exactly when the desired shape is
214
+ * fully reflected on disk: gate-enforce present AND no legacy
215
+ * progress-start present.
216
+ */
217
+ function shapeMatchesDesired(settings, ide) {
218
+ const desiredEntries = resolveHookEntries(ide);
219
+ const desiredSentinels = new Set(desiredEntries.map((e) => e.sentinel));
220
+ const allPeaksSentinels = resolveLegacySentinels(ide);
221
+ const eventKeys = new Set(resolveHookEntries(ide).map((e) => e.event));
222
+ for (const eventKey of eventKeys) {
223
+ const present = readHookEventEntries(settings, eventKey);
224
+ const peaksPresent = present.filter((e) => entryIsPeaksManaged(e, allPeaksSentinels));
225
+ // (a) every peaks-managed entry currently on disk must match the
226
+ // desired sentinel set (no stale entries the caller wants removed).
227
+ for (const entry of peaksPresent) {
228
+ const entrySentinels = (entry.hooks ?? []).map((h) => allPeaksSentinels.find((s) => String(h.command ?? '').includes(s))).filter((s) => Boolean(s));
229
+ if (entrySentinels.some((s) => !desiredSentinels.has(s))) {
230
+ return false;
231
+ }
232
+ }
233
+ // (b) every desired entry must be on disk.
234
+ for (const sentinel of desiredSentinels) {
235
+ const has = peaksPresent.some((entry) => (entry.hooks ?? []).some((h) => String(h.command ?? '').includes(sentinel)));
236
+ if (!has)
237
+ return false;
238
+ }
239
+ }
240
+ return true;
241
+ }
136
242
  export function planHookInstall(scope, projectRoot, options) {
137
243
  const ide = resolveIde(options);
244
+ const _skipProgress = resolveSkipProgress(options);
138
245
  const root = resolveSettingsRoot(scope, projectRoot);
139
246
  const settingsPath = resolveSettingsPath(scope, ide, projectRoot);
140
247
  assertSafeSettingsPathCompat(scope, ide, root, settingsPath);
@@ -152,24 +259,29 @@ export function planHookInstall(scope, projectRoot, options) {
152
259
  };
153
260
  }
154
261
  /** Merge all peaks-managed hook entries into settings, preserving all other keys and hooks. */
155
- function withHooksInstalledForIde(settings, ide) {
262
+ function withHooksInstalledForIde(settings, ide, _skipProgress = false) {
156
263
  const existingHooks = (settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks))
157
264
  ? settings.hooks
158
265
  : {};
159
- // Per-IDE entries may map to different events (Trae: both on beforeToolCall;
160
- // Claude: both on PreToolUse). Group by event so each event array is
161
- // independently merged.
266
+ // Per-IDE entries may map to different events (Trae: gate-enforce on
267
+ // beforeToolCall; Claude: gate-enforce on PreToolUse). Group by event
268
+ // so each event array is independently merged.
162
269
  const ourByEvent = new Map();
163
270
  for (const spec of resolveHookEntries(ide)) {
164
271
  const list = ourByEvent.get(spec.event) ?? [];
165
272
  list.push(spec);
166
273
  ourByEvent.set(spec.event, list);
167
274
  }
168
- const sentinels = resolveHookEntries(ide).map((e) => e.sentinel);
275
+ // Slice #014: the legacy sentinel set includes the progress-start
276
+ // sentinel so a pre-#014 install's stale progress-start entry is
277
+ // stripped by the filter (the file converges on the new
278
+ // gate-enforce-only shape, idempotently). The desired set (passed
279
+ // in below) only contains the gate-enforce sentinel.
280
+ const allSentinels = resolveLegacySentinels(ide);
169
281
  const nextHooks = { ...existingHooks };
170
282
  for (const [eventKey, ourEntries] of ourByEvent) {
171
283
  const existing = readHookEntriesFromHooks(nextHooks, eventKey);
172
- const nonPeaks = existing.filter((entry) => !entryIsPeaksManaged(entry, sentinels));
284
+ const nonPeaks = existing.filter((entry) => !entryIsPeaksManaged(entry, allSentinels));
173
285
  const ourFormatted = ourEntries.map((spec) => ({
174
286
  matcher: spec.matcher,
175
287
  hooks: [{ type: 'command', command: spec.command }]
@@ -189,17 +301,25 @@ function withHooksInstalledForIde(settings, ide) {
189
301
  }
190
302
  export function applyHookInstall(scope, projectRoot, options) {
191
303
  const ide = resolveIde(options);
304
+ const _skipProgress = resolveSkipProgress(options);
192
305
  const root = resolveSettingsRoot(scope, projectRoot);
193
306
  const settingsPath = resolveSettingsPath(scope, ide, projectRoot);
194
307
  assertSafeSettingsPathCompat(scope, ide, root, settingsPath);
195
308
  const exists = existsSync(settingsPath);
196
309
  const settings = exists ? readJsonObjectFile(settingsPath) : {};
197
310
  const spec = resolveHookSpec(ide);
311
+ // Slice #014: `alreadyInstalled` reflects the FULL desired shape
312
+ // (gate-enforce-only + no stale progress-start entry). Pre-#014
313
+ // installs that left a progress-start entry behind will be treated
314
+ // as not-yet-installed, so the merge strips the stale entry on the
315
+ // next install call. This is the only path that converges the file
316
+ // on the new shape; pure presence-checks are insufficient.
317
+ const alreadyInstalled = shapeMatchesDesired(settings, ide);
198
318
  const baseResult = {
199
319
  scope,
200
320
  settingsPath,
201
321
  exists,
202
- alreadyInstalled: isInstalledForIde(settings, ide),
322
+ alreadyInstalled,
203
323
  desiredCommand: spec.hookEnforceCommand,
204
324
  sentinel: spec.hookEnforceSentinel,
205
325
  matcher: spec.hookEnforceMatcher
@@ -220,7 +340,12 @@ export function removeHookInstall(scope, projectRoot, options) {
220
340
  }
221
341
  const settings = readJsonObjectFile(settingsPath);
222
342
  const existingHooks = settings.hooks ?? {};
223
- const sentinels = resolveHookEntries(ide).map((e) => e.sentinel);
343
+ // Slice #014: uninstall must remove the gate-enforce entry AND any
344
+ // legacy progress-start entry that a pre-#014 install left behind.
345
+ // The legacy sentinel set covers both shapes so the uninstall
346
+ // converges the file on "no peaks-managed entries", regardless of
347
+ // what shape the file was in when the user ran uninstall.
348
+ const sentinels = resolveLegacySentinels(ide);
224
349
  const eventKeys = new Set(resolveHookEntries(ide).map((e) => e.event));
225
350
  let removedAny = false;
226
351
  const nextHooks = { ...existingHooks };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * IDE-aware wrapper for `peaks standards init` / `peaks standards update`.
3
+ *
4
+ * Slice #011-2026-06-07-ide-adapter-resource-profile: the original
5
+ * `executeProjectStandardsInit` / `executeProjectStandardsUpdate` always
6
+ * wrote to `CLAUDE.md` + `.claude/rules/**` regardless of which IDE
7
+ * the user was running. This wrapper dispatches on the IDE detected
8
+ * (or explicitly requested via `--ide`) and falls back to the legacy
9
+ * Claude Code path with a stderr warning when the detected IDE has
10
+ * no `standardsProfile` declared (Trae in slice 1.3.2).
11
+ *
12
+ * Two entry points:
13
+ *
14
+ * - `executeProjectStandardsInitIdeAware` — same signature as the
15
+ * underlying `executeProjectStandardsInit`, plus an optional
16
+ * `ideId` override that bypasses detection.
17
+ * - `executeProjectStandardsUpdateIdeAware` — same shape, for the
18
+ * `update` flow.
19
+ *
20
+ * Detection precedence:
21
+ * 1. Explicit `options.ideId` (CLI `--ide` flag)
22
+ * 2. `IdeRegistry.detect()` from cwd (or the `projectRoot` if given)
23
+ * 3. `null` (no IDE detected) → fall back to the legacy Claude Code path
24
+ *
25
+ * Fallback behavior: when the resolved IDE has no `standardsProfile`
26
+ * declared, the wrapper STILL calls the legacy Claude Code writer
27
+ * (so the user gets the files they would have gotten before slice #011)
28
+ * and emits a stderr warning with the IDE id and the fact that the
29
+ * adapter is UNVERIFIED for the standards profile. This keeps the
30
+ * "Trae is UNVERIFIED, ship a working file tree, surface the gap"
31
+ * contract intact.
32
+ */
33
+ import type { IdeId } from '../ide/ide-types.js';
34
+ import { detectAllResourceTargets, getStandardsProfile } from '../ide/resource-profile.js';
35
+ import { type ProjectStandardsInitOptions, type ProjectStandardsInitResult, type ProjectStandardsUpdateResult } from './project-standards-service.js';
36
+ export type { ProjectStandardsInitResult, ProjectStandardsUpdateResult };
37
+ export type ProjectStandardsIdeAwareOptions = ProjectStandardsInitOptions & {
38
+ /**
39
+ * Explicit IDE override. When set, bypasses `IdeRegistry.detect()`
40
+ * (cwd + env heuristics) and uses the provided IDE id directly.
41
+ * Mirrors the `peaks hooks install --ide <id>` pattern.
42
+ */
43
+ readonly ideId?: IdeId;
44
+ };
45
+ export type ProjectStandardsUpdateIdeAwareOptions = ProjectStandardsInitOptions & {
46
+ /** Explicit IDE override. See {@link ProjectStandardsIdeAwareOptions}. */
47
+ readonly ideId?: IdeId;
48
+ };
49
+ /**
50
+ * Resolve the active IDE id for a standards call. Order of precedence:
51
+ * 1. explicit `options.ideId`
52
+ * 2. `IdeRegistry.detect()` from `options.projectRoot`
53
+ * 3. `null` (no detected IDE — caller falls back to legacy)
54
+ */
55
+ export declare function resolveStandardsIdeId(options: {
56
+ readonly projectRoot: string;
57
+ readonly ideId?: IdeId;
58
+ }): IdeId | null;
59
+ /**
60
+ * Run `peaks standards init` with IDE-aware dispatch.
61
+ *
62
+ * When the resolved IDE has a `standardsProfile`, the call still
63
+ * delegates to the existing `executeProjectStandardsInit` (the
64
+ * profile maps to the Claude Code path; future per-IDE writers
65
+ * plug in here). When the IDE is unregistered for the standards
66
+ * profile, the call delegates to the legacy path + emits a stderr
67
+ * warning.
68
+ */
69
+ export declare function executeProjectStandardsInitIdeAware(options: ProjectStandardsIdeAwareOptions): ProjectStandardsInitResult;
70
+ /**
71
+ * Run `peaks standards update` with IDE-aware dispatch.
72
+ *
73
+ * Same dispatch rules as `executeProjectStandardsInitIdeAware`.
74
+ */
75
+ export declare function executeProjectStandardsUpdateIdeAware(options: ProjectStandardsUpdateIdeAwareOptions): ProjectStandardsUpdateResult;
76
+ /**
77
+ * Test seam + integration-test helper: returns the resolved IDE id
78
+ * for the call, plus the active standards profile. Exported for
79
+ * the integration test in `tests/unit/standards/ide-aware-standards-service.test.ts`
80
+ * to assert the dispatch decision without running the full write.
81
+ */
82
+ export declare function inspectStandardsDispatch(options: {
83
+ readonly projectRoot: string;
84
+ readonly ideId?: IdeId;
85
+ }): {
86
+ readonly ideId: IdeId | null;
87
+ readonly profile: ReturnType<typeof getStandardsProfile>;
88
+ };
89
+ /**
90
+ * Test seam: the resource-profile accessor exposes
91
+ * `detectAllResourceTargets` for callers that need to enumerate
92
+ * across all registered IDEs. Re-exported here for convenience.
93
+ */
94
+ export { detectAllResourceTargets };
@@ -0,0 +1,89 @@
1
+ import { detectAllResourceTargets, getStandardsProfile, } from '../ide/resource-profile.js';
2
+ import { executeProjectStandardsInit, executeProjectStandardsUpdate, } from './project-standards-service.js';
3
+ import { detectInstalledIde } from '../ide/ide-detector.js';
4
+ function warnUnregisteredIde(ideId, projectRoot) {
5
+ process.stderr.write(`peaks standards: IDE '${ideId}' has no standardsProfile declared; ` +
6
+ `falling back to the legacy Claude Code path (CLAUDE.md + .claude/rules/**) ` +
7
+ `for project '${projectRoot}'. This is a slice #011 follow-up gap; ` +
8
+ `see .peaks/memory/ide-adapter-resource-profile-framework.md.\n`);
9
+ }
10
+ function warnNoIdeDetected(projectRoot) {
11
+ process.stderr.write(`peaks standards: no IDE detected in '${projectRoot}'; ` +
12
+ `writing to the legacy Claude Code path (CLAUDE.md + .claude/rules/**). ` +
13
+ `Pass --ide <id> to bypass detection.\n`);
14
+ }
15
+ /**
16
+ * Resolve the active IDE id for a standards call. Order of precedence:
17
+ * 1. explicit `options.ideId`
18
+ * 2. `IdeRegistry.detect()` from `options.projectRoot`
19
+ * 3. `null` (no detected IDE — caller falls back to legacy)
20
+ */
21
+ export function resolveStandardsIdeId(options) {
22
+ if (options.ideId !== undefined) {
23
+ return options.ideId;
24
+ }
25
+ return detectInstalledIde(options.projectRoot);
26
+ }
27
+ /**
28
+ * Run `peaks standards init` with IDE-aware dispatch.
29
+ *
30
+ * When the resolved IDE has a `standardsProfile`, the call still
31
+ * delegates to the existing `executeProjectStandardsInit` (the
32
+ * profile maps to the Claude Code path; future per-IDE writers
33
+ * plug in here). When the IDE is unregistered for the standards
34
+ * profile, the call delegates to the legacy path + emits a stderr
35
+ * warning.
36
+ */
37
+ export function executeProjectStandardsInitIdeAware(options) {
38
+ const ideId = resolveStandardsIdeId(options);
39
+ if (ideId === null) {
40
+ warnNoIdeDetected(options.projectRoot);
41
+ return executeProjectStandardsInit(options);
42
+ }
43
+ const profile = getStandardsProfile(ideId);
44
+ if (profile === null) {
45
+ warnUnregisteredIde(ideId, options.projectRoot);
46
+ return executeProjectStandardsInit(options);
47
+ }
48
+ // Claude Code path: profile matches the legacy writer. Future per-IDE
49
+ // writers (markdown+frontmatter, multiple rule roots, etc.) plug in
50
+ // here by branching on `profile.format` / `profile.rulesDir`.
51
+ return executeProjectStandardsInit(options);
52
+ }
53
+ /**
54
+ * Run `peaks standards update` with IDE-aware dispatch.
55
+ *
56
+ * Same dispatch rules as `executeProjectStandardsInitIdeAware`.
57
+ */
58
+ export function executeProjectStandardsUpdateIdeAware(options) {
59
+ const ideId = resolveStandardsIdeId(options);
60
+ if (ideId === null) {
61
+ warnNoIdeDetected(options.projectRoot);
62
+ return executeProjectStandardsUpdate(options);
63
+ }
64
+ const profile = getStandardsProfile(ideId);
65
+ if (profile === null) {
66
+ warnUnregisteredIde(ideId, options.projectRoot);
67
+ return executeProjectStandardsUpdate(options);
68
+ }
69
+ return executeProjectStandardsUpdate(options);
70
+ }
71
+ /**
72
+ * Test seam + integration-test helper: returns the resolved IDE id
73
+ * for the call, plus the active standards profile. Exported for
74
+ * the integration test in `tests/unit/standards/ide-aware-standards-service.test.ts`
75
+ * to assert the dispatch decision without running the full write.
76
+ */
77
+ export function inspectStandardsDispatch(options) {
78
+ const ideId = resolveStandardsIdeId(options);
79
+ if (ideId === null) {
80
+ return { ideId: null, profile: null };
81
+ }
82
+ return { ideId, profile: getStandardsProfile(ideId) };
83
+ }
84
+ /**
85
+ * Test seam: the resource-profile accessor exposes
86
+ * `detectAllResourceTargets` for callers that need to enumerate
87
+ * across all registered IDEs. Re-exported here for convenience.
88
+ */
89
+ export { detectAllResourceTargets };
@@ -68,7 +68,7 @@ export type ProjectStandardsUpdateSummary = {
68
68
  readonly reviewSuggestions: string[];
69
69
  };
70
70
  };
71
- type ProjectStandardsInitOptions = {
71
+ export type ProjectStandardsInitOptions = {
72
72
  readonly projectRoot: string;
73
73
  readonly language?: string;
74
74
  readonly apply?: boolean;
@@ -79,4 +79,3 @@ export declare function executeProjectStandardsInit(options: ProjectStandardsIni
79
79
  export declare function executeProjectStandardsUpdate(options: ProjectStandardsInitOptions): ProjectStandardsUpdateResult;
80
80
  export declare function summarizeProjectStandardsInitResult(result: ProjectStandardsInitResult): ProjectStandardsInitSummary;
81
81
  export declare function summarizeProjectStandardsUpdateResult(result: ProjectStandardsUpdateResult): ProjectStandardsUpdateSummary;
82
- export {};
@@ -11,14 +11,6 @@
11
11
  * @returns Next available number (1, 2, 3, ...)
12
12
  */
13
13
  export declare function getNextNumber(dirPath: string): number;
14
- /**
15
- * Build a numbered filename from a number and description.
16
- * Format: 001-description-slug.md
17
- *
18
- * @param number - The file number (will be zero-padded to 3 digits)
19
- * @param description - Human-readable description (converted to kebab-case slug)
20
- * @returns Formatted filename like "001-feature-name.md"
21
- */
22
14
  export declare function buildNumberedFilename(number: number, description: string): string;
23
15
  /**
24
16
  * Get the next numbered file path in a directory.
@@ -34,13 +34,23 @@ export function getNextNumber(dirPath) {
34
34
  * @param description - Human-readable description (converted to kebab-case slug)
35
35
  * @returns Formatted filename like "001-feature-name.md"
36
36
  */
37
+ // Windows supports up to 255 chars per filename component (and 260 for the
38
+ // full path). Pre-#015 the slug was silently truncated to 50 chars, which
39
+ // produced orphaned artefacts (the on-disk file no longer matched the
40
+ // request-id and the state machine could not find it). 255 is the OS-level
41
+ // ceiling; if a requestId exceeds that, `mkdir` / `writeFile` will surface a
42
+ // real ENAMETOOLONG error instead of a silent mismatch. We reserve 4 chars
43
+ // for the `<NNN>-` numeric prefix and 3 chars for the `.md` suffix, so
44
+ // the slug may be at most 248 chars (giving 4 + 248 + 3 = 255 total).
45
+ const MAX_FILENAME_LENGTH = 255;
46
+ const MAX_FILENAME_SLUG_LENGTH = MAX_FILENAME_LENGTH - 7;
37
47
  export function buildNumberedFilename(number, description) {
38
48
  const padded = String(number).padStart(3, '0');
39
49
  const slug = description
40
50
  .toLowerCase()
41
51
  .replace(/[^a-z0-9]+/g, '-')
42
52
  .replace(/^-|-$/g, '')
43
- .slice(0, 50); // Limit slug length
53
+ .slice(0, MAX_FILENAME_SLUG_LENGTH);
44
54
  return `${padded}-${slug}.md`;
45
55
  }
46
56
  /**
@@ -1 +1 @@
1
- export declare const CLI_VERSION = "1.3.3";
1
+ export declare const CLI_VERSION = "1.3.5";
@@ -1 +1 @@
1
- export const CLI_VERSION = "1.3.3";
1
+ export const CLI_VERSION = "1.3.5";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-cli",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "description": "Cross-AI-IDE workflow-gating CLI + skill family (Claude Code shipped, Trae in progress; Codex / Cursor / Qoder / Tongyi Lingma on the roadmap).",
5
5
  "author": "SquabbyZ",
6
6
  "license": "MIT",