peaks-loop 4.1.0 → 4.1.1

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 (70) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/cli/commands/_register.js +2 -2
  5. package/dist/cli/commands/code-mode-gate-should-pause-command.js +1 -1
  6. package/dist/cli/commands/comments-commands.d.ts +14 -0
  7. package/dist/cli/commands/comments-commands.js +96 -0
  8. package/dist/cli/commands/core/memory-command.js +10 -0
  9. package/dist/cli/commands/core/skill-command.js +1 -1
  10. package/dist/cli/commands/core/standards-command.js +1 -1
  11. package/dist/cli/commands/ecc-commands.d.ts +17 -22
  12. package/dist/cli/commands/ecc-commands.js +38 -26
  13. package/dist/cli/commands/prd-commands.js +8 -2
  14. package/dist/cli/program.js +4 -9
  15. package/dist/services/code/mode-gate-types.d.ts +1 -1
  16. package/dist/services/code/mode-gate-types.js +0 -1
  17. package/dist/services/code/mode-gate.js +1 -9
  18. package/dist/services/code/user-touchpoint-classifier.js +0 -7
  19. package/dist/services/code-review/ecc-bridge.d.ts +6 -6
  20. package/dist/services/comments/citation-rules.d.ts +154 -0
  21. package/dist/services/comments/citation-rules.js +241 -0
  22. package/dist/services/comments/comment-audit.d.ts +57 -0
  23. package/dist/services/comments/comment-audit.js +100 -0
  24. package/dist/services/comments/comment-citations.d.ts +60 -0
  25. package/dist/services/comments/comment-citations.js +186 -0
  26. package/dist/services/comments/comment-hygiene.d.ts +79 -0
  27. package/dist/services/comments/comment-hygiene.js +133 -0
  28. package/dist/services/comments/comment-prune.d.ts +88 -0
  29. package/dist/services/comments/comment-prune.js +148 -0
  30. package/dist/services/comments/prune-apply.d.ts +60 -0
  31. package/dist/services/comments/prune-apply.js +150 -0
  32. package/dist/services/comments/repo-path-probe.d.ts +43 -0
  33. package/dist/services/comments/repo-path-probe.js +78 -0
  34. package/dist/services/log/retention.d.ts +0 -16
  35. package/dist/services/log/retention.js +0 -17
  36. package/dist/services/memory/project-memory-service/index.d.ts +1 -1
  37. package/dist/services/memory/project-memory-service/index.js +1 -1
  38. package/dist/services/memory/project-memory-service/store/atomic-write.d.ts +19 -7
  39. package/dist/services/memory/project-memory-service/store/atomic-write.js +120 -26
  40. package/dist/services/prd/handoff-frontmatter.js +61 -0
  41. package/dist/services/prd/handoff-gate-evidence.js +14 -10
  42. package/dist/services/prd/handoff-service.d.ts +11 -1
  43. package/dist/services/prd/handoff-service.js +11 -1
  44. package/dist/services/prd/handoff-types.d.ts +33 -1
  45. package/dist/services/recommendations/installed-capability-detector.d.ts +5 -5
  46. package/dist/services/recommendations/installed-capability-detector.js +11 -10
  47. package/dist/services/scan/archetype-detection.d.ts +37 -0
  48. package/dist/services/scan/archetype-detection.js +175 -2
  49. package/dist/services/scan/archetype-service.js +36 -22
  50. package/dist/services/scan/scan-types.d.ts +9 -0
  51. package/dist/services/workspace/generated-artifacts-stamp.d.ts +2 -2
  52. package/dist/services/workspace/generated-artifacts-stamp.js +2 -2
  53. package/dist/services/workspace/workspace-service.js +1 -1
  54. package/package.json +5 -5
  55. package/scripts/install-skills.mjs +0 -177
  56. package/skills/bee/peaks-qa/references/reading-handoff-frontmatter.md +3 -1
  57. package/skills/bee/peaks-rd/references/parallel-review-fanout.md +1 -1
  58. package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +3 -2
  59. package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +29 -7
  60. package/skills/peaks-code/references/frontend-only-mode.md +2 -2
  61. package/skills/peaks-code/references/startup-sequence.md +0 -4
  62. package/dist/cli/commands/upgrade-commands.d.ts +0 -25
  63. package/dist/cli/commands/upgrade-commands.js +0 -154
  64. package/dist/services/upgrade/1x-detector-service.d.ts +0 -7
  65. package/dist/services/upgrade/1x-detector-service.js +0 -96
  66. package/dist/services/upgrade/gitignore-migrate-service.d.ts +0 -56
  67. package/dist/services/upgrade/gitignore-migrate-service.js +0 -170
  68. package/dist/services/upgrade/upgrade-service.d.ts +0 -81
  69. package/dist/services/upgrade/upgrade-service.js +0 -428
  70. package/skills/peaks-code/references/step-0-55-1x-detection.md +0 -83
@@ -1,428 +0,0 @@
1
- /**
2
- * peaks upgrade --to 2.0 — umbrella service for the 1.x → 2.0
3
- * migration.
4
- *
5
- * Per the "one-key completion" + "minimal-user-operation" tenets
6
- * (2026-06-11), the typical upgrade path is:
7
- *
8
- * $ npm i -g peaks-loop@2.0 # postinstall does everything
9
- *
10
- * OR (postinstall skipped / manual fallback):
11
- *
12
- * $ peaks upgrade --to 2.0
13
- *
14
- * The umbrella orchestrates 7 sub-commands:
15
- * 1. config migrate (already ships as `peaks config migrate`)
16
- * 2. standards migrate (`peaks standards migrate --from-claude-rules`)
17
- * 3. memory extract (already ships as `peaks memory extract`)
18
- * 4. hooks install (already ships as `peaks hooks install`)
19
- * 5. skill sync (this session, `peaks skill sync --all`)
20
- * 6. audit verify (already ships as `peaks audit red-lines`)
21
- * 7. write upgrade record (in-process, .peaks/memory/upgrade-2.0-*.md)
22
- *
23
- * Each sub-step is a thin shell-out to the existing CLI; the
24
- * umbrella's only in-process work is the audit and the upgrade
25
- * record write. Sub-step failures are SOFT (logged + nextActions
26
- * populated) so the umbrella never blocks a successful partial
27
- * upgrade.
28
- */
29
- import { spawnSync } from 'node:child_process';
30
- import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
31
- import { join, dirname, relative, resolve } from 'node:path';
32
- import { fileURLToPath } from 'node:url';
33
- import { runRedLinesAudit } from '../audit/red-lines-service.js';
34
- import { savePreferences } from '../preferences/preferences-service.js';
35
- import { migrateGitignoreFile } from './gitignore-migrate-service.js';
36
- /**
37
- * Default substep executor — wraps `spawnSync` with the same
38
- * stdio / encoding / timeout config the umbrella has used since
39
- * v1. Exported for test coverage: the seam-preservation test
40
- * imports this directly to verify the normalized return shape
41
- * on a spawn failure (e.g. a non-existent command).
42
- */
43
- export const defaultSubstepExecutor = (command, args, timeoutMs) => {
44
- const start = Date.now();
45
- try {
46
- const result = spawnSync(command, args, {
47
- encoding: 'utf8',
48
- stdio: ['ignore', 'pipe', 'pipe'],
49
- timeout: timeoutMs,
50
- windowsHide: true
51
- });
52
- return {
53
- status: result.status,
54
- stdout: result.stdout ?? '',
55
- stderr: result.stderr ?? '',
56
- durationMs: Date.now() - start
57
- };
58
- }
59
- catch (err) {
60
- const message = err instanceof Error ? err.message : String(err);
61
- return {
62
- status: null,
63
- stdout: '',
64
- stderr: message,
65
- durationMs: Date.now() - start
66
- };
67
- }
68
- };
69
- const STEPS = [
70
- {
71
- name: 'config-migrate',
72
- args: (p) => ['config', 'migrate', '--project', p, '--apply', '--json']
73
- },
74
- {
75
- name: 'standards-migrate',
76
- args: (p) => [
77
- 'standards',
78
- 'migrate',
79
- '--from-claude-rules',
80
- '--project',
81
- p,
82
- '--apply',
83
- '--json'
84
- ]
85
- },
86
- // memory extract is special: its --artifact takes literal file
87
- // paths (memory-service rejects glob patterns via realpathSync).
88
- // The umbrella expands the three documented patterns
89
- // (skills/**/SKILL.md, CLAUDE.md, .claude/rules/**/*.md) on disk
90
- // and passes the resulting literal list. See runUpgrade's special
91
- // case below for the args resolution; the args function here is
92
- // a placeholder so the STEPS table stays uniform.
93
- { name: 'memory-extract', args: (p) => ['memory', 'extract', '--project', p, '--json'] },
94
- { name: 'hooks-install', args: (p) => ['hooks', 'install', '--project', p, '--json'] },
95
- { name: 'skill-sync', args: (p) => ['skill', 'sync', '--all', '--project', p, '--json'] },
96
- { name: 'audit-verify', args: (p) => ['audit', 'red-lines', '--project', p, '--json'] }
97
- ];
98
- /**
99
- * Walk `<root>` recursively and collect every file whose basename
100
- * matches `predicate`. Returns absolute paths.
101
- *
102
- * Mirrors `readMarkdownFilesRecursive` in
103
- * src/services/standards/migrate-claude-rules-service.ts so the
104
- * umbrella does not pull a new glob dependency (Node 20+ engine
105
- * constraint — `fs.globSync` requires Node 22+).
106
- */
107
- function collectFilesRecursive(root, predicate) {
108
- if (!existsSync(root))
109
- return [];
110
- const stat = statSync(root);
111
- if (stat.isFile()) {
112
- return predicate(root.split(/[\\/]/).pop() ?? '') ? [root] : [];
113
- }
114
- if (!stat.isDirectory())
115
- return [];
116
- const out = [];
117
- for (const entry of readdirSync(root)) {
118
- const child = join(root, entry);
119
- let childStat;
120
- try {
121
- childStat = statSync(child);
122
- }
123
- catch {
124
- continue;
125
- }
126
- if (childStat.isFile()) {
127
- if (predicate(entry))
128
- out.push(child);
129
- }
130
- else if (childStat.isDirectory()) {
131
- out.push(...collectFilesRecursive(child, predicate));
132
- }
133
- }
134
- return out;
135
- }
136
- /**
137
- * Resolve the three documented memory-extract artifact patterns
138
- * against a real project tree. Returns project-relative paths
139
- * (memory-service joins them with --project root) so the
140
- * realpathSync inside memory-service's assertInsideProject
141
- * succeeds.
142
- *
143
- * Patterns:
144
- * - skills/[asterisk][asterisk]/SKILL.md (project-root convention)
145
- * - .claude/skills/[asterisk][asterisk]/SKILL.md (Claude-Code consumer convention; ice-cola)
146
- * - CLAUDE.md
147
- * - .claude/rules/[asterisk][asterisk]/[asterisk].md
148
- *
149
- * Returns an empty list when none of the roots exist. The
150
- * caller marks the step skipped in that case.
151
- */
152
- function expandMemoryArtifacts(projectRoot) {
153
- const out = [];
154
- // skills/**/SKILL.md (peaks-loop repo convention)
155
- const skillFiles = collectFilesRecursive(join(projectRoot, 'skills'), (name) => name === 'SKILL.md');
156
- for (const abs of skillFiles) {
157
- out.push(relative(projectRoot, abs));
158
- }
159
- // .claude/skills/**/SKILL.md (Claude-Code consumer convention;
160
- // surfaced by ice-cola dogfood 2026-06-12 — the 1.x install
161
- // landed skills under .claude/skills/, not <root>/skills/)
162
- const claudeSkillFiles = collectFilesRecursive(join(projectRoot, '.claude', 'skills'), (name) => name === 'SKILL.md');
163
- for (const abs of claudeSkillFiles) {
164
- out.push(relative(projectRoot, abs));
165
- }
166
- // CLAUDE.md (literal)
167
- const claudeMd = join(projectRoot, 'CLAUDE.md');
168
- if (existsSync(claudeMd) && statSync(claudeMd).isFile()) {
169
- out.push('CLAUDE.md');
170
- }
171
- // .claude/rules/**/*.md
172
- const claudeRules = collectFilesRecursive(join(projectRoot, '.claude', 'rules'), (name) => name.endsWith('.md'));
173
- for (const abs of claudeRules) {
174
- out.push(relative(projectRoot, abs));
175
- }
176
- return out;
177
- }
178
- function read1xVersion(cwd) {
179
- const home = process.env['HOME'] ?? process.env['USERPROFILE'] ?? '';
180
- if (home.length === 0)
181
- return null;
182
- const global = join(home, '.peaks', 'config.json');
183
- if (!existsSync(global))
184
- return null;
185
- try {
186
- const raw = JSON.parse(readFileSync(global, 'utf8'));
187
- if (typeof raw.version === 'string')
188
- return raw.version;
189
- }
190
- catch {
191
- // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
192
- // ignore
193
- }
194
- return null;
195
- }
196
- function runStep(peaksBin, name, args, timeoutMs = 60_000, executor = defaultSubstepExecutor) {
197
- // The global `peaks` shim is a `/bin/sh` symlink script (the
198
- // npm install postinstall creates `peaks` → `peaks.sh` on
199
- // Windows). cmd.exe (the default Windows shell) cannot run
200
- // `.sh` scripts directly, so the shim fails with "unknown
201
- // command 'migrate'" etc. The fix: prefer the local node
202
- // binary + the peaks.js script path. The umbrella resolves
203
- // the script path at startup; only falls back to `peaks` if
204
- // no script path is available (Unix-only).
205
- let command;
206
- let spawnArgs;
207
- if (peaksBin.includes('\\') || peaksBin.includes('/')) {
208
- // peaksBin is a real path (e.g. /c/.../bin/peaks.js);
209
- // invoke directly via node.
210
- command = process.execPath;
211
- spawnArgs = [peaksBin, ...args];
212
- }
213
- else {
214
- // peaksBin is just "peaks" — best-effort shell exec.
215
- command = peaksBin;
216
- spawnArgs = args;
217
- }
218
- // The executor normalizes spawn errors to { status: null, stderr: ... }
219
- // so no try/catch wrapper is needed here — the call always returns
220
- // a shaped record. See defaultSubstepExecutor for the reference impl.
221
- const result = executor(command, spawnArgs, timeoutMs);
222
- return {
223
- name,
224
- status: result.status === 0 ? 'pass' : 'fail',
225
- exitCode: result.status,
226
- stdout: result.stdout,
227
- stderr: result.stderr,
228
- durationMs: result.durationMs
229
- };
230
- }
231
- function writeUpgradeRecord(projectRoot, result) {
232
- try {
233
- const memoryDir = join(projectRoot, '.peaks', 'memory');
234
- mkdirSync(memoryDir, { recursive: true });
235
- const date = new Date().toISOString().slice(0, 10);
236
- const file = join(memoryDir, `upgrade-2.0-${date}.md`);
237
- const lines = [];
238
- lines.push(`# Upgrade to peaks-loop 2.0 — ${date}`);
239
- lines.push('');
240
- lines.push(`> Auto-generated by \`peaks upgrade --to 2.0${result.applied ? ' --auto' : ''}\`.`);
241
- lines.push(`> Per the "one-key completion" + "minimal-user-operation" tenets.`);
242
- lines.push('');
243
- if (result.fromVersion !== null) {
244
- lines.push(`**From version**: ${result.fromVersion}`);
245
- }
246
- lines.push(`**To version**: 2.0.0`);
247
- lines.push(`**Project root**: \`${result.projectRoot}\``);
248
- lines.push('');
249
- lines.push('## Sub-step results');
250
- lines.push('');
251
- lines.push('| step | status | exitCode | durationMs |');
252
- lines.push('|------|--------|----------|------------|');
253
- for (const step of result.steps) {
254
- lines.push(`| ${step.name} | ${step.status} | ${step.exitCode ?? 'n/a'} | ${step.durationMs} |`);
255
- }
256
- lines.push('');
257
- if (result.auditBefore !== null || result.auditAfter !== null) {
258
- lines.push('## Audit snapshot');
259
- lines.push('');
260
- if (result.auditBefore !== null) {
261
- lines.push(`- Before: totalRedLines=${result.auditBefore.totalRedLines}, cliBacked=${result.auditBefore.cliBacked}`);
262
- }
263
- if (result.auditAfter !== null) {
264
- lines.push(`- After: totalRedLines=${result.auditAfter.totalRedLines}, cliBacked=${result.auditAfter.cliBacked}`);
265
- }
266
- lines.push('');
267
- }
268
- lines.push('## Next actions');
269
- lines.push('');
270
- for (const a of result.nextActions) {
271
- lines.push(`- ${a}`);
272
- }
273
- writeFileSync(file, lines.join('\n') + '\n', 'utf8');
274
- return file;
275
- }
276
- catch (err) {
277
- process.stderr.write(`peaks upgrade: failed to write upgrade record: ${err instanceof Error ? err.message : String(err)}\n`);
278
- return null;
279
- }
280
- }
281
- export function runUpgrade(input) {
282
- // Resolve the peaks binary. Default: the peaks.js script
283
- // co-located with this compiled module (the user just installed
284
- // peaks-loop globally, but the global `peaks` shim is a .sh
285
- // script that cmd.exe can't run on Windows). Falling back
286
- // to just "peaks" lets the umbrella work when invoked from
287
- // a Unix-style environment that can run the shim directly.
288
- const here = dirname(fileURLToPath(import.meta.url));
289
- // Walk up from the compiled location to find bin/peaks.js.
290
- // The compiled service lives at dist/services/upgrade/upgrade-service.js
291
- // (rootDir=src in tsconfig.build.json trims the "src" segment); bin/peaks.js
292
- // is at the peaks-loop root, 3 dirs up.
293
- const peaksBin = input.peaksBin ?? resolve(here, '..', '..', '..', 'bin', 'peaks.js');
294
- const fallbackPeaks = 'peaks';
295
- const resolvedPeaksBin = existsSync(peaksBin) ? peaksBin : fallbackPeaks;
296
- // Resolve the executor once at the top of runUpgrade so every
297
- // sub-step call shares the same instance. Tests pass a fake
298
- // executor via UpgradeInput.executor; production callers omit
299
- // it and get the default spawnSync wrapper.
300
- const executor = input.executor ?? defaultSubstepExecutor;
301
- const fromVersion = read1xVersion(input.projectRoot);
302
- const steps = [];
303
- const warnings = [];
304
- const nextActions = [];
305
- // Ensure .peaks/preferences.json exists. This is the file the
306
- // 1.x detector keys off — without it, `peaks upgrade --detect-1x`
307
- // keeps returning isOneX=true after a successful upgrade and the
308
- // user gets stuck in a re-prompt loop. savePreferences with an
309
- // empty override merges with DEFAULT_PREFERENCES and writes; if
310
- // the file already exists the user's values are preserved.
311
- // Real bug surfaced by ice-cola dogfood 2026-06-12.
312
- try {
313
- savePreferences(input.projectRoot, {});
314
- }
315
- catch (err) {
316
- warnings.push(`ensure-preferences failed: ${err instanceof Error ? err.message : String(err)}`);
317
- }
318
- // Migrate .gitignore so 2.0 tracked artifacts
319
- // (.peaks/standards/, .peaks/memory/*.md durable memories,
320
- // .peaks/PROJECT.md) aren't silently hidden by a 1.x wholesale
321
- // `/.peaks/` ignore rule. Real bug surfaced by ice-cola dogfood
322
- // 2026-06-12: every consumer artifact was being dropped from git
323
- // status. Service is idempotent + creates a timestamped backup
324
- // before any write.
325
- try {
326
- const giResult = migrateGitignoreFile({ projectRoot: input.projectRoot, apply: true });
327
- if (giResult.changed && giResult.appliedWrite && giResult.backupPath !== null) {
328
- nextActions.push(`Updated .gitignore — removed stale wholesale .peaks rule(s): ${giResult.removedRules.join(', ')}. Backup at ${giResult.backupPath}.`);
329
- }
330
- else if (giResult.missing) {
331
- warnings.push('gitignore-migrate skipped: project has no .gitignore');
332
- }
333
- }
334
- catch (err) {
335
- warnings.push(`gitignore-migrate failed: ${err instanceof Error ? err.message : String(err)}`);
336
- }
337
- // Audit BEFORE the upgrade (baseline)
338
- let auditBefore = null;
339
- try {
340
- const r = runRedLinesAudit({ projectRoot: input.projectRoot });
341
- auditBefore = { totalRedLines: r.audit.totalRedLines, cliBacked: r.audit.cliBacked };
342
- }
343
- catch (err) {
344
- warnings.push(`audit-before failed: ${err instanceof Error ? err.message : String(err)}`);
345
- }
346
- // Run the 6 sub-steps
347
- for (const step of STEPS) {
348
- if (step.name === 'memory-extract') {
349
- // Special case: expand the three glob patterns to literal
350
- // paths before spawning. memory-service rejects literal
351
- // '**' in artifact paths (assertInsideProject's realpathSync
352
- // throws ENOENT) and refuses to run without --artifact.
353
- const artifacts = expandMemoryArtifacts(input.projectRoot);
354
- if (artifacts.length === 0) {
355
- steps.push({
356
- name: 'memory-extract',
357
- status: 'skipped',
358
- exitCode: null,
359
- stdout: '',
360
- stderr: 'no skills/, CLAUDE.md, or .claude/rules/ artifacts found in the project',
361
- durationMs: 0
362
- });
363
- continue;
364
- }
365
- const args = [
366
- 'memory',
367
- 'extract',
368
- '--project',
369
- input.projectRoot,
370
- '--artifact',
371
- ...artifacts,
372
- '--apply',
373
- '--json'
374
- ];
375
- const r = runStep(resolvedPeaksBin, 'memory-extract', args, 60_000, executor);
376
- steps.push(r);
377
- if (r.status === 'fail') {
378
- warnings.push(`memory-extract failed: ${r.stderr.slice(0, 200)}`);
379
- }
380
- continue;
381
- }
382
- const args = step.args(input.projectRoot);
383
- const r = runStep(resolvedPeaksBin, step.name, args, 60_000, executor);
384
- steps.push(r);
385
- if (r.status === 'fail') {
386
- warnings.push(`${step.name} failed: ${r.stderr.slice(0, 200)}`);
387
- }
388
- }
389
- // Audit AFTER the upgrade (verify)
390
- let auditAfter = null;
391
- try {
392
- const r = runRedLinesAudit({ projectRoot: input.projectRoot });
393
- auditAfter = { totalRedLines: r.audit.totalRedLines, cliBacked: r.audit.cliBacked };
394
- }
395
- catch (err) {
396
- warnings.push(`audit-after failed: ${err instanceof Error ? err.message : String(err)}`);
397
- }
398
- const passedCount = steps.filter((s) => s.status === 'pass').length;
399
- const failedCount = steps.filter((s) => s.status === 'fail').length;
400
- const skippedCount = steps.filter((s) => s.status === 'skipped').length;
401
- const applied = failedCount === 0;
402
- if (failedCount > 0) {
403
- nextActions.push(`${failedCount} sub-step(s) failed. Run \`peaks upgrade --to 2.0\` again to retry the failed steps.`);
404
- }
405
- if (input.auto !== true) {
406
- nextActions.push('Run `peaks audit red-lines --project .` to verify the L2 catalog is healthy.');
407
- }
408
- nextActions.push('See `docs/UPGRADING-2.0.md` for the manual fallback if this auto-upgrade fails.');
409
- // Write the upgrade record (always, even on partial failure —
410
- // the user gets a forensic artifact either way)
411
- const partial = {
412
- applied,
413
- fromVersion,
414
- toVersion: '2.0.0',
415
- projectRoot: input.projectRoot,
416
- steps,
417
- passedCount,
418
- failedCount,
419
- skippedCount,
420
- auditBefore,
421
- auditAfter,
422
- upgradeRecordPath: null,
423
- nextActions,
424
- warnings
425
- };
426
- const upgradeRecordPath = writeUpgradeRecord(input.projectRoot, partial);
427
- return { ...partial, upgradeRecordPath };
428
- }
@@ -1,83 +0,0 @@
1
- # Step 0.55 — 1.x → 2.0 detection reference
2
-
3
- > Body of `### Peaks-Loop Step 0.55`. Slice: 2026-06-12-code-step-0-55-1x-detection.
4
-
5
- ## Why this step exists
6
-
7
- The peaks-loop 1.x → 2.0 closeout ships:
8
-
9
- 1. A postinstall that auto-detects 1.x state and dispatches the upgrade (slice 1, commit `b6e34e6`).
10
- 2. A standards-migrate path that thins `.claude/rules/**/*.md` and scaffolds `.peaks/standards/` (slice 2, commit `33dd392`).
11
- 3. **THIS STEP** — a peaks-code startup sequence probe that detects 1.x state when the user invokes `/peaks-code` directly in a 1.x consumer project, and prompts the user to upgrade.
12
-
13
- The 1.x user experience is: "I just typed /peaks-code. Why is it not working in 2.0 mode?" Step 0.55 catches this case and surfaces an `AskUserQuestion` with a one-click upgrade.
14
-
15
- ## Detection algorithm
16
-
17
- ```bash
18
- # 1. Read-only probe via the umbrella CLI
19
- peaks upgrade --detect-1x --project <root> --json
20
- ```
21
-
22
- The CLI returns:
23
- ```json
24
- {
25
- "ok": true,
26
- "command": "upgrade.detect-1x",
27
- "data": {
28
- "isOneX": true,
29
- "signals": [
30
- "<path> has schema_version 1.0.0, expected '2.0.0'",
31
- "..."
32
- ],
33
- "projectRoot": "/path/to/project",
34
- "configPath": null
35
- },
36
- "warnings": [],
37
- "nextActions": [
38
- "Detected 1.x state. peaks-code Step 0.55 should present an AskUserQuestion to invoke `peaks upgrade --to 2.0 --auto --project /path/to/project`."
39
- ]
40
- }
41
- ```
42
-
43
- The detection logic mirrors `scripts/install-skills.mjs:detect1xProjectState` (canonical implementation) — it walks up to find `.peaks/_runtime/`, then sniffs:
44
-
45
- 1. `~/.peaks/config.json` for `version: 1.x`
46
- 2. `<projectRoot>/.claude/rules/common/dev-preference.md` for "peaks progress"
47
- 3. `<projectRoot>/.peaks/preferences.json` for missing or non-`2.0.0` `schema_version`
48
-
49
- The TS mirror in `src/services/upgrade/1x-detector-service.ts` is the canonical entrypoint for the skill; the postinstall `.mjs` version is the canonical entrypoint for the npm-install flow. The two implementations MUST stay in parity (a parity test is in the slice's test suite).
50
-
51
- ## AskUserQuestion (only when `isOneX: true`)
52
-
53
- | Option | What it does |
54
- |---|---|
55
- | Run `peaks upgrade --to 2.0 --auto --project <root>` (Recommended) | Invokes the umbrella. The user sees the 6 sub-step results in the terminal. After the upgrade, re-run peaks-code with the standing 2.0 layout. Persist `autoUpgradePrompt: opt-in` to `.peaks/preferences.json`. |
56
- | Skip for this session | Continue with the standing 1.x layout. Persist `autoUpgradePrompt: skip-this-session` to `.peaks/preferences.json`. The next time the user invokes peaks-code in this project, the question re-asks. |
57
- | Never ask again for this project | Persist `autoUpgradePrompt: skip-forever` to `.peaks/preferences.json`. Step 0.55 becomes a no-op for this project from now on. The user can re-enable later by removing the `autoUpgradePrompt` key from preferences.json. |
58
-
59
- ## Persistence contract
60
-
61
- The decision is persisted via `peaks preferences set --project <root> --key autoUpgradePrompt --value <opt-in|skip-this-session|skip-forever> --apply`. Subsequent Step 0.55 invocations read the value first:
62
-
63
- - If `opt-in` (and Step 0.55 is the first invocation in the session), the user has already opted in; auto-run the umbrella without re-asking.
64
- - If `skip-this-session`, the user already said no this session; skip without re-asking (but re-ask next session).
65
- - If `skip-forever`, the user said no permanently; skip without re-asking.
66
- - If the key is absent, present the AskUserQuestion.
67
-
68
- ## What is NOT in this step
69
-
70
- - The auto-upgrade execution itself: the umbrella is invoked via the AskUserQuestion's recommended option. The umbrella's behavior (6 sub-commands + write-upgrade-record) is documented in the umbrella's own help text.
71
- - The postinstall auto-dispatch: that's `scripts/install-skills.mjs:autoUpgrade1xProjectIfPresent`, which fires fire-and-forget on `npm i -g peaks-loop@2.0`. Step 0.55 is the user-invoked path.
72
- - Re-authoring the 1.x detector heuristics: the implementation is a 1:1 mirror of the canonical `.mjs` version. Drift is prevented by the parity test.
73
-
74
- ## How this integrates with the rest of the workflow
75
-
76
- - Step 0 (anchor) — runs always.
77
- - Step 0.7 (resume detection) — runs after Step 0.
78
- - **Step 0.55 (1.x detection) — runs after Step 0.7, only when the project is not on a 2.0 layout.**
79
- - Step 1 (mode selection) — runs after Step 0.55.
80
-
81
- The 1.x detection is intentionally placed AFTER Step 0.7 because the user might already have a 1.x-converted in-flight slice; the resume flow takes precedence over the upgrade prompt.
82
-
83
- > **Historical note (prior to 2026-07-08 RR slice):** an OpenSpec first-run opt-in step (Step 0.5) used to sit between Step 0 and Step 0.7. peaks-code is now decoupled from OpenSpec and does not surface that step; this reference still preserves the historical ordering for context.