peaks-cli 1.2.9 → 1.3.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 (53) hide show
  1. package/README.md +62 -46
  2. package/bin/peaks.js +0 -0
  3. package/dist/src/cli/commands/hooks-commands.js +24 -9
  4. package/dist/src/cli/commands/progress-commands.js +26 -2
  5. package/dist/src/cli/commands/request-commands.js +5 -0
  6. package/dist/src/cli/commands/slice-commands.d.ts +3 -0
  7. package/dist/src/cli/commands/slice-commands.js +42 -0
  8. package/dist/src/cli/commands/workflow-commands.js +3 -3
  9. package/dist/src/cli/commands/workspace-commands.d.ts +63 -0
  10. package/dist/src/cli/commands/workspace-commands.js +347 -5
  11. package/dist/src/cli/program.js +4 -0
  12. package/dist/src/services/artifacts/artifact-prerequisites.d.ts +17 -1
  13. package/dist/src/services/artifacts/artifact-prerequisites.js +38 -5
  14. package/dist/src/services/artifacts/request-artifact-service.d.ts +22 -0
  15. package/dist/src/services/artifacts/request-artifact-service.js +172 -54
  16. package/dist/src/services/doctor/doctor-service.d.ts +7 -0
  17. package/dist/src/services/doctor/doctor-service.js +20 -2
  18. package/dist/src/services/progress/progress-service.d.ts +26 -0
  19. package/dist/src/services/progress/progress-service.js +25 -0
  20. package/dist/src/services/sc/sc-service.d.ts +52 -1
  21. package/dist/src/services/sc/sc-service.js +324 -17
  22. package/dist/src/services/scan/acceptance-coverage-service.js +6 -2
  23. package/dist/src/services/session/session-manager.d.ts +7 -5
  24. package/dist/src/services/session/session-manager.js +60 -16
  25. package/dist/src/services/skills/hooks-settings-service.d.ts +25 -3
  26. package/dist/src/services/skills/hooks-settings-service.js +57 -13
  27. package/dist/src/services/skills/skill-presence-service.js +102 -68
  28. package/dist/src/services/skills/skill-runbook-service.js +2 -1
  29. package/dist/src/services/skills/skill-statusline-service.js +13 -7
  30. package/dist/src/services/slice/slice-check-service.d.ts +2 -0
  31. package/dist/src/services/slice/slice-check-service.js +248 -0
  32. package/dist/src/services/slice/slice-check-types.d.ts +61 -0
  33. package/dist/src/services/slice/slice-check-types.js +18 -0
  34. package/dist/src/services/workflow/pipeline-verify-service.d.ts +5 -2
  35. package/dist/src/services/workflow/pipeline-verify-service.js +35 -35
  36. package/dist/src/services/workspace/migrate-service.d.ts +2 -0
  37. package/dist/src/services/workspace/migrate-service.js +484 -0
  38. package/dist/src/services/workspace/migrate-types.d.ts +84 -0
  39. package/dist/src/services/workspace/migrate-types.js +21 -0
  40. package/dist/src/services/workspace/reconcile-service.d.ts +119 -0
  41. package/dist/src/services/workspace/reconcile-service.js +464 -0
  42. package/dist/src/services/workspace/reconcile-types.d.ts +93 -0
  43. package/dist/src/services/workspace/reconcile-types.js +13 -0
  44. package/dist/src/services/workspace/workspace-service.d.ts +11 -0
  45. package/dist/src/services/workspace/workspace-service.js +87 -7
  46. package/dist/src/shared/change-id.d.ts +59 -0
  47. package/dist/src/shared/change-id.js +194 -16
  48. package/dist/src/shared/version.d.ts +1 -1
  49. package/dist/src/shared/version.js +1 -1
  50. package/package.json +13 -2
  51. package/skills/peaks-solo/SKILL.md +28 -4
  52. package/skills/peaks-solo/references/micro-cycle.md +155 -0
  53. package/skills/peaks-solo/references/runbook.md +2 -0
@@ -8,6 +8,15 @@ export type WorkspaceInitOptions = {
8
8
  * — the CLI surfaces this as `--allow-session-rebind`.
9
9
  */
10
10
  allowSessionRebind?: boolean;
11
+ /**
12
+ * Optional change-id to bind as the active unit of work. When set,
13
+ * `peaks workspace init` also writes a `.peaks/_runtime/current-change`
14
+ * symlink pointing at `.peaks/<changeId>/`, so RD/QA/PRD services
15
+ * know which `<change-id>` directory to write reviewable artifacts
16
+ * into. The session id is still the binding for ephemeral state
17
+ * (live sub-agent progress, spawn records).
18
+ */
19
+ changeId?: string;
11
20
  };
12
21
  export type WorkspaceInitReport = {
13
22
  sessionId: string;
@@ -16,6 +25,8 @@ export type WorkspaceInitReport = {
16
25
  alreadyExisted: string[];
17
26
  bound: boolean;
18
27
  previousSessionId: string | null;
28
+ changeId: string | null;
29
+ changeIdAction: 'bound' | 'preserved' | 'none';
19
30
  };
20
31
  export declare class InvalidSessionIdError extends Error {
21
32
  readonly code = "INVALID_SESSION_ID";
@@ -2,7 +2,16 @@ import { mkdir } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { isDirectory } from '../../shared/fs.js';
4
4
  import { getSessionId, setCurrentSessionBinding } from '../session/session-manager.js';
5
- const SUBDIRECTORIES = [
5
+ import { setCurrentChangeId } from '../../shared/change-id.js';
6
+ /**
7
+ * Per-slice subdirectories created **inside the change-id dir**
8
+ * (`.peaks/<change-id>/...`). These are the reviewable
9
+ * artifacts and are tracked in git. The `system/` subdir is
10
+ * intentionally NOT in this list — it lives under the session
11
+ * dir (`.peaks/_runtime/<session-id>/system/`), since it holds
12
+ * live sub-agent progress and spawn records, which are ephemeral.
13
+ */
14
+ const CHANGE_ARTIFACT_SUBDIRECTORIES = [
6
15
  'prd/source',
7
16
  'prd/requests',
8
17
  'ui/requests',
@@ -12,7 +21,14 @@ const SUBDIRECTORIES = [
12
21
  'qa/requests',
13
22
  'qa/screenshots',
14
23
  'sc',
15
- 'txt',
24
+ 'txt'
25
+ ];
26
+ /**
27
+ * Per-session subdirectories created **inside the session dir**
28
+ * (`.peaks/_runtime/<session-id>/...`). These are the ephemeral
29
+ * state and are gitignored.
30
+ */
31
+ const SESSION_EPHEMERAL_SUBDIRECTORIES = [
16
32
  'system'
17
33
  ];
18
34
  const SESSION_ID_PATTERN = /^\d{4}-\d{2}-\d{2}-[a-z][a-z0-9-]*[a-z0-9]$/;
@@ -61,9 +77,26 @@ export function validateSessionId(sessionId) {
61
77
  }
62
78
  export async function initWorkspace(options) {
63
79
  validateSessionId(options.sessionId);
64
- const sessionRoot = join(options.projectRoot, '.peaks', options.sessionId);
80
+ // Phase 6 refactor (slice 2026-06-05-change-id-as-unit-of-work):
81
+ // - Reviewable artifacts (rd/, qa/, prd/, txt/) are created at
82
+ // `.peaks/<change-id>/<role>/` (tracked in git) when a change-id
83
+ // is given. This is the canonical home for cross-session content.
84
+ // - The session dir `.peaks/_runtime/<session-id>/` (gitignored)
85
+ // holds only ephemeral state — currently `system/` (live
86
+ // sub-agent progress + spawn records). The session id remains
87
+ // the binding for that ephemeral state.
88
+ //
89
+ // The CLI accepts `--change-id <id>` to bind the change. The legacy
90
+ // session-scoped layout (`.peaks/<session-id>/<role>/<file>`) is
91
+ // no longer used by writes; pre-1.3.1 trees get their session
92
+ // files migrated to the change-id dir by `peaks workspace reconcile`.
93
+ const runtimeRoot = join(options.projectRoot, '.peaks', '_runtime');
94
+ const sessionRoot = join(runtimeRoot, options.sessionId);
65
95
  const created = [];
66
96
  const alreadyExisted = [];
97
+ // 1. Create the session dir (canonical location `.peaks/_runtime/<sid>/`)
98
+ // with ONLY ephemeral subdirs (`system/`). The session dir is
99
+ // gitignored.
67
100
  if (await isDirectory(sessionRoot)) {
68
101
  alreadyExisted.push('.');
69
102
  }
@@ -71,7 +104,7 @@ export async function initWorkspace(options) {
71
104
  await mkdir(sessionRoot, { recursive: true });
72
105
  created.push('.');
73
106
  }
74
- for (const sub of SUBDIRECTORIES) {
107
+ for (const sub of SESSION_EPHEMERAL_SUBDIRECTORIES) {
75
108
  const full = join(sessionRoot, sub);
76
109
  if (await isDirectory(full)) {
77
110
  alreadyExisted.push(sub);
@@ -81,7 +114,45 @@ export async function initWorkspace(options) {
81
114
  created.push(sub);
82
115
  }
83
116
  }
84
- // Bind this session as the project's current one.
117
+ // 2. If a change-id is given, also create the change-id dir at
118
+ // `.peaks/<change-id>/` (tracked) with the reviewable subdirs.
119
+ // When the caller did NOT specify a change-id, this step is
120
+ // skipped — reviewable writes for this session are then blocked
121
+ // until a change-id is bound (or the user re-runs init with
122
+ // `--change-id`). Surfaces in `changeIdAction: 'none'`.
123
+ let resolvedChangeId = null;
124
+ let changeIdAction = 'none';
125
+ if (options.changeId !== undefined && options.changeId.length > 0) {
126
+ resolvedChangeId = options.changeId;
127
+ const changeDir = join(options.projectRoot, '.peaks', resolvedChangeId);
128
+ if (await isDirectory(changeDir)) {
129
+ alreadyExisted.push(resolvedChangeId);
130
+ }
131
+ else {
132
+ await mkdir(changeDir, { recursive: true });
133
+ created.push(resolvedChangeId);
134
+ }
135
+ for (const sub of CHANGE_ARTIFACT_SUBDIRECTORIES) {
136
+ const full = join(changeDir, sub);
137
+ if (await isDirectory(full)) {
138
+ alreadyExisted.push(sub);
139
+ }
140
+ else {
141
+ await mkdir(full, { recursive: true });
142
+ created.push(sub);
143
+ }
144
+ }
145
+ // 3. Bind the change-id so RD/QA/PRD services know where to write
146
+ // reviewable artifacts. The binding is a symlink at
147
+ // `.peaks/_runtime/current-change` pointing at the change-id dir.
148
+ setCurrentChangeId(options.projectRoot, resolvedChangeId);
149
+ changeIdAction = 'bound';
150
+ }
151
+ else if (options.changeId !== undefined && options.changeId.length === 0) {
152
+ // Empty string — same as undefined; treat as no change-id.
153
+ changeIdAction = 'none';
154
+ }
155
+ // 4. Bind this session as the project's current one.
85
156
  //
86
157
  // Single source of truth: `peaks workspace init` is the only CLI entry point
87
158
  // that takes an explicit --session-id, so it owns the binding to .session.json.
@@ -112,7 +183,7 @@ export async function initWorkspace(options) {
112
183
  // etc.) regardless of whether the per-session metadata file is present.
113
184
  // Refuse to rebind without explicit authorization.
114
185
  previousSessionId = existingSessionId;
115
- const existingSessionDir = join(options.projectRoot, '.peaks', existingSessionId);
186
+ const existingSessionDir = join(runtimeRoot, existingSessionId);
116
187
  if (await isDirectory(existingSessionDir) && !options.allowSessionRebind) {
117
188
  const { readdirSync } = await import('node:fs');
118
189
  const entries = readdirSync(existingSessionDir);
@@ -127,5 +198,14 @@ export async function initWorkspace(options) {
127
198
  setCurrentSessionBinding(options.projectRoot, options.sessionId);
128
199
  bound = true;
129
200
  }
130
- return { sessionId: options.sessionId, sessionRoot, created, alreadyExisted, bound, previousSessionId };
201
+ return {
202
+ sessionId: options.sessionId,
203
+ sessionRoot,
204
+ created,
205
+ alreadyExisted,
206
+ bound,
207
+ previousSessionId,
208
+ changeId: resolvedChangeId,
209
+ changeIdAction
210
+ };
131
211
  }
@@ -2,6 +2,20 @@
2
2
  * Shared change-id validation and artifact path helpers.
3
3
  * All Peaks planner commands must use these to prevent path traversal
4
4
  * and keep artifacts inside the Peaks artifact workspace.
5
+ *
6
+ * Layout (as of slice 2026-06-05-change-id-as-unit-of-work):
7
+ * - Reviewable artifacts (rd/, qa/, prd/, txt/, prd/source/):
8
+ * .peaks/<change-id>/<role>/... (tracked in git)
9
+ * - Ephemeral state (live sub-agent progress, spawn records):
10
+ * .peaks/_runtime/<session-id>/... (gitignored)
11
+ * - The active change-id binding lives at `.peaks/_runtime/current-change`
12
+ * (symlink pointing at `.peaks/<change-id>/` for one-minor-release back-compat
13
+ * also accepts a plain file with the change-id as its sole content).
14
+ *
15
+ * The session id remains in use as a binding (which developer's local
16
+ * working session is active) but it is NOT the durable scope for
17
+ * reviewable content — change-id is. Sessions are ephemeral and
18
+ * gitignored; changes are durable and tracked.
5
19
  */
6
20
  export declare function isValidChangeId(changeId: string): boolean;
7
21
  export declare function isUnsafePathInput(input: string): boolean;
@@ -56,3 +70,48 @@ export declare function buildArtifactRelativePathInRoot(projectRoot: string, cha
56
70
  */
57
71
  export declare function buildArtifactRelativePath(changeId: string, ...segments: string[]): string;
58
72
  export declare function isPathInsideArtifactRoot(path: string, artifactRoot: string): boolean;
73
+ /**
74
+ * Read the active change-id binding for a project. Returns null when
75
+ * no binding exists or the binding is malformed / escapes the project
76
+ * root. As of slice 2026-06-05-change-id-as-unit-of-work this is the
77
+ * primary routing key for reviewable artifact writes — RD/QA/PRD
78
+ * services should call this (rather than guess from the session id)
79
+ * to decide which `.peaks/<change-id>/` directory to write into.
80
+ */
81
+ export declare function getCurrentChangeId(projectRoot: string): string | null;
82
+ /**
83
+ * Source of the resolved change-id binding. Useful for tests that
84
+ * need to confirm whether the binding came from the canonical
85
+ * `_runtime/current-change` symlink or the legacy `current-change`
86
+ * plain-file path.
87
+ */
88
+ export declare function getCurrentChangeIdSource(projectRoot: string): {
89
+ changeId: string;
90
+ source: 'symlink' | 'file';
91
+ } | null;
92
+ /**
93
+ * Write a change-id binding for a project. Two forms are supported
94
+ * (the same as `getCurrentChangeId` reads):
95
+ *
96
+ * - `{ form: 'symlink' }` (default): creates
97
+ * `.peaks/_runtime/current-change` as a symlink pointing at
98
+ * `.peaks/<changeId>/`. Requires the target dir to exist (the
99
+ * caller is responsible for `initWorkspace` + the change-id dir).
100
+ * - `{ form: 'file' }`: writes the change-id as the sole content of
101
+ * `.peaks/_runtime/current-change`. The legacy plain-file form.
102
+ *
103
+ * Idempotent: re-running with the same changeId + form is a no-op.
104
+ * Re-running with a different changeId on an existing symlink throws —
105
+ * the caller must remove the binding first (or use a different path).
106
+ */
107
+ export declare function setCurrentChangeId(projectRoot: string, changeId: string, options?: {
108
+ form?: 'symlink' | 'file';
109
+ }): void;
110
+ /**
111
+ * Canonical on-disk path to a change-id's reviewable artifacts
112
+ * (`.peaks/<change-id>/`). Writes that target reviewable content
113
+ * (RD/QA/PRD/txt) should land here regardless of which session
114
+ * is active. Ephemeral state (live sub-agent progress, spawn records)
115
+ * stays in the session dir (`.peaks/_runtime/<session-id>/...`).
116
+ */
117
+ export declare function getChangeArtifactRoot(projectRoot: string, changeId: string): string;
@@ -2,11 +2,25 @@
2
2
  * Shared change-id validation and artifact path helpers.
3
3
  * All Peaks planner commands must use these to prevent path traversal
4
4
  * and keep artifacts inside the Peaks artifact workspace.
5
+ *
6
+ * Layout (as of slice 2026-06-05-change-id-as-unit-of-work):
7
+ * - Reviewable artifacts (rd/, qa/, prd/, txt/, prd/source/):
8
+ * .peaks/<change-id>/<role>/... (tracked in git)
9
+ * - Ephemeral state (live sub-agent progress, spawn records):
10
+ * .peaks/_runtime/<session-id>/... (gitignored)
11
+ * - The active change-id binding lives at `.peaks/_runtime/current-change`
12
+ * (symlink pointing at `.peaks/<change-id>/` for one-minor-release back-compat
13
+ * also accepts a plain file with the change-id as its sole content).
14
+ *
15
+ * The session id remains in use as a binding (which developer's local
16
+ * working session is active) but it is NOT the durable scope for
17
+ * reviewable content — change-id is. Sessions are ephemeral and
18
+ * gitignored; changes are durable and tracked.
5
19
  */
6
- import { posix, join } from 'node:path';
7
- import { getNextNumber, buildNumberedFilename } from './incrementing-number.js';
8
- import { getSessionId } from '../services/session/session-manager.js';
20
+ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
21
+ import { posix, join, basename } from 'node:path';
9
22
  import { findProjectRoot } from '../services/config/config-safety.js';
23
+ import { isInsidePath } from './path-utils.js';
10
24
  const CHANGE_ID_PATTERN = /^[A-Za-z0-9._-]+$/;
11
25
  function normalizeForwardSlashes(input) {
12
26
  return input.replace(/\\/g, '/');
@@ -86,22 +100,21 @@ export function isUnsafeArtifactPath(path) {
86
100
  */
87
101
  export function buildArtifactRelativePathInRoot(projectRoot, changeId, ...segments) {
88
102
  validateChangeIdOrThrow(changeId);
103
+ // As of slice 2026-06-05-change-id-as-unit-of-work, reviewable
104
+ // artifacts (RD/QA/PRD/txt) are routed to the change-id-scoped
105
+ // directory `.peaks/<change-id>/<segments-joined>`. The session id
106
+ // is the binding for ephemeral state (live sub-agent progress,
107
+ // spawn records) only and is NOT part of the reviewable-artifact
108
+ // path. Pre-1.3.1 trees get their old session-scoped files migrated
109
+ // to the change-id dir by `peaks workspace reconcile`.
89
110
  const resolvedProjectRoot = projectRoot && projectRoot.length > 0
90
111
  ? projectRoot
91
112
  : (findProjectRoot(process.cwd()) ?? process.cwd());
92
- const sessionId = getSessionId(resolvedProjectRoot);
93
- if (sessionId && segments.length > 0 && segments[0]) {
94
- const role = normalizeForwardSlashes(segments[0]);
95
- const dirPath = join(resolvedProjectRoot, '.peaks', sessionId, role);
96
- if (isUnsafeArtifactPath(role) || isUnsafeArtifactPath(sessionId)) {
97
- throw new ChangeIdValidationError(changeId);
98
- }
99
- const number = getNextNumber(dirPath);
100
- const filename = buildNumberedFilename(number, changeId);
101
- const candidatePath = `.peaks/${sessionId}/${role}/${filename}`;
102
- return normalizeArtifactPath(candidatePath);
103
- }
104
- // Fallback: no session or no segments - use legacy behavior
113
+ // Use segments verbatim as the sub-path. This preserves the
114
+ // legacy behavior where `buildArtifactRelativePath(changeId, 'rd', 'architecture')`
115
+ // produces `.peaks/<changeId>/rd/architecture` (the caller specifies
116
+ // the full sub-path, including any custom filename like
117
+ // `architecture`, `001-foo.md`, `swarm/workers/rd-impl-001`, etc.).
105
118
  const joined = segments.map((segment) => normalizeForwardSlashes(segment)).join('/');
106
119
  const candidatePath = `.peaks/${changeId}/${joined}`;
107
120
  if (isUnsafeArtifactPath(joined) || isUnsafeArtifactPath(candidatePath)) {
@@ -138,3 +151,168 @@ export function isPathInsideArtifactRoot(path, artifactRoot) {
138
151
  const normalizedRoot = normalizeArtifactPath(artifactRoot);
139
152
  return normalizedPath === normalizedRoot || normalizedPath.startsWith(`${normalizedRoot}/`);
140
153
  }
154
+ // ---------------------------------------------------------------------------
155
+ // Change-id binding (.peaks/_runtime/current-change)
156
+ // ---------------------------------------------------------------------------
157
+ //
158
+ // The active change-id binding lives at `.peaks/_runtime/current-change`.
159
+ // Two forms are accepted (back-compat for the legacy file-based binding
160
+ // that pre-dates the runtime-layer refactor):
161
+ //
162
+ // 1. Symlink: the symlink target resolves to `.peaks/<change-id>/`
163
+ // inside the project root. This is the canonical form that
164
+ // `peaks workspace init --change-id <id>` writes.
165
+ //
166
+ // 2. Plain file: the file's first non-empty line is the change-id.
167
+ // Older `peaks workspace init` (pre-1.3.1) wrote the change-id
168
+ // as a plain file at `.peaks/current-change`. We still read it.
169
+ //
170
+ // In either case the change-id is validated against CHANGE_ID_PATTERN
171
+ // (letters/digits/dots/underscores/dashes, no `..`) and the resolved
172
+ // path must stay inside the project root (defense against a symlink
173
+ // pointing outside).
174
+ //
175
+ // The binding is read by RD/QA/PRD services when they need to know
176
+ // which `.peaks/<change-id>/` directory to write reviewable artifacts
177
+ // into, and by reconciliation to figure out which slice each legacy
178
+ // session file belongs to.
179
+ const CURRENT_CHANGE_REL = '_runtime/current-change';
180
+ const LEGACY_CURRENT_CHANGE_REL = 'current-change';
181
+ const CHANGE_DIR_PATTERN = /^[A-Za-z0-9._-]+$/;
182
+ function safeReadBinding(projectRoot) {
183
+ const peaksRoot = join(projectRoot, '.peaks');
184
+ const realPeaks = (() => {
185
+ try {
186
+ return realpathSync(peaksRoot);
187
+ }
188
+ catch {
189
+ return peaksRoot;
190
+ }
191
+ })();
192
+ for (const rel of [CURRENT_CHANGE_REL, LEGACY_CURRENT_CHANGE_REL]) {
193
+ const bindingPath = join(peaksRoot, rel);
194
+ if (!existsSync(bindingPath))
195
+ continue;
196
+ try {
197
+ const stat = lstatSync(bindingPath);
198
+ if (stat.isSymbolicLink()) {
199
+ const targetPath = realpathSync(bindingPath);
200
+ if (!isInsidePath(targetPath, realPeaks))
201
+ return null;
202
+ const targetId = basename(targetPath);
203
+ if (!CHANGE_DIR_PATTERN.test(targetId) || targetId === '.' || targetId === '..')
204
+ return null;
205
+ return { changeId: targetId, source: 'symlink' };
206
+ }
207
+ const raw = readFileSync(bindingPath, 'utf-8').trim();
208
+ if (!raw || !CHANGE_DIR_PATTERN.test(raw) || raw === '.' || raw === '..')
209
+ return null;
210
+ return { changeId: raw, source: 'file' };
211
+ }
212
+ catch {
213
+ return null;
214
+ }
215
+ }
216
+ return null;
217
+ }
218
+ /**
219
+ * Read the active change-id binding for a project. Returns null when
220
+ * no binding exists or the binding is malformed / escapes the project
221
+ * root. As of slice 2026-06-05-change-id-as-unit-of-work this is the
222
+ * primary routing key for reviewable artifact writes — RD/QA/PRD
223
+ * services should call this (rather than guess from the session id)
224
+ * to decide which `.peaks/<change-id>/` directory to write into.
225
+ */
226
+ export function getCurrentChangeId(projectRoot) {
227
+ return safeReadBinding(projectRoot)?.changeId ?? null;
228
+ }
229
+ /**
230
+ * Source of the resolved change-id binding. Useful for tests that
231
+ * need to confirm whether the binding came from the canonical
232
+ * `_runtime/current-change` symlink or the legacy `current-change`
233
+ * plain-file path.
234
+ */
235
+ export function getCurrentChangeIdSource(projectRoot) {
236
+ return safeReadBinding(projectRoot);
237
+ }
238
+ /**
239
+ * Write a change-id binding for a project. Two forms are supported
240
+ * (the same as `getCurrentChangeId` reads):
241
+ *
242
+ * - `{ form: 'symlink' }` (default): creates
243
+ * `.peaks/_runtime/current-change` as a symlink pointing at
244
+ * `.peaks/<changeId>/`. Requires the target dir to exist (the
245
+ * caller is responsible for `initWorkspace` + the change-id dir).
246
+ * - `{ form: 'file' }`: writes the change-id as the sole content of
247
+ * `.peaks/_runtime/current-change`. The legacy plain-file form.
248
+ *
249
+ * Idempotent: re-running with the same changeId + form is a no-op.
250
+ * Re-running with a different changeId on an existing symlink throws —
251
+ * the caller must remove the binding first (or use a different path).
252
+ */
253
+ export function setCurrentChangeId(projectRoot, changeId, options = {}) {
254
+ validateChangeIdOrThrow(changeId);
255
+ const form = options.form ?? 'symlink';
256
+ const peaksRoot = join(projectRoot, '.peaks');
257
+ const bindingPath = join(peaksRoot, CURRENT_CHANGE_REL);
258
+ // Ensure `_runtime/` exists.
259
+ const runtimeDir = join(peaksRoot, '_runtime');
260
+ if (!existsSync(runtimeDir)) {
261
+ // Lazy import: do not pull fs/promises at the top to keep the
262
+ // module's import graph minimal.
263
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
264
+ const { mkdirSync } = require('node:fs');
265
+ mkdirSync(runtimeDir, { recursive: true });
266
+ }
267
+ if (form === 'file') {
268
+ writeFileSync(bindingPath, changeId + '\n', 'utf-8');
269
+ return;
270
+ }
271
+ // symlink form: point at .peaks/<changeId>/
272
+ const targetDir = join(peaksRoot, changeId);
273
+ if (!existsSync(targetDir)) {
274
+ mkdirSync(targetDir, { recursive: true });
275
+ }
276
+ if (existsSync(bindingPath)) {
277
+ // Re-running with a different changeId is a configuration error;
278
+ // surface it loudly so the caller (peaks workspace init) can decide
279
+ // whether to --allow-session-rebind for the underlying session.
280
+ try {
281
+ const existing = readFileSync(bindingPath, 'utf-8').trim();
282
+ if (existing !== changeId) {
283
+ if (existsSync(join(peaksRoot, changeId))) {
284
+ unlinkSync(bindingPath);
285
+ }
286
+ else {
287
+ throw new Error(`current-change binding points at "${existing}" but caller asked to set "${changeId}". ` +
288
+ `Remove .peaks/_runtime/current-change first or pass the existing changeId.`);
289
+ }
290
+ }
291
+ else {
292
+ return; // identical — no-op
293
+ }
294
+ }
295
+ catch (error) {
296
+ if (error instanceof Error && error.message.startsWith('current-change binding points'))
297
+ throw error;
298
+ // Could not read the existing binding (e.g. it's a broken symlink).
299
+ // Replace.
300
+ try {
301
+ unlinkSync(bindingPath);
302
+ }
303
+ catch { /* best effort */ }
304
+ }
305
+ }
306
+ symlinkSync(targetDir, bindingPath);
307
+ }
308
+ /**
309
+ * Canonical on-disk path to a change-id's reviewable artifacts
310
+ * (`.peaks/<change-id>/`). Writes that target reviewable content
311
+ * (RD/QA/PRD/txt) should land here regardless of which session
312
+ * is active. Ephemeral state (live sub-agent progress, spawn records)
313
+ * stays in the session dir (`.peaks/_runtime/<session-id>/...`).
314
+ */
315
+ export function getChangeArtifactRoot(projectRoot, changeId) {
316
+ validateChangeIdOrThrow(changeId);
317
+ return join(projectRoot, '.peaks', changeId);
318
+ }
@@ -1 +1 @@
1
- export declare const CLI_VERSION = "1.2.9";
1
+ export declare const CLI_VERSION = "1.3.1";
@@ -1 +1 @@
1
- export const CLI_VERSION = "1.2.9";
1
+ export const CLI_VERSION = "1.3.1";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "peaks-cli",
3
- "version": "1.2.9",
4
- "description": "Peaks CLI and short skill family for Claude Code automation.",
3
+ "version": "1.3.1",
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",
7
7
  "type": "module",
@@ -9,6 +9,14 @@
9
9
  "publishConfig": {
10
10
  "access": "public"
11
11
  },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/SquabbyZ/peaks-cli.git"
15
+ },
16
+ "homepage": "https://github.com/SquabbyZ/peaks-cli",
17
+ "bugs": {
18
+ "url": "https://github.com/SquabbyZ/peaks-cli/issues"
19
+ },
12
20
  "bin": {
13
21
  "peaks": "./bin/peaks.js"
14
22
  },
@@ -30,9 +38,12 @@
30
38
  "scripts": {
31
39
  "build": "node ./scripts/sync-version.mjs && node ./scripts/clean-dist.mjs && tsc -p tsconfig.json",
32
40
  "prepack": "npm run build",
41
+ "prepublish": "node ./scripts/sync-version.mjs",
33
42
  "postinstall": "node ./scripts/install-skills.mjs",
43
+ "predev": "node ./scripts/sync-version.mjs",
34
44
  "dev": "tsx src/cli/index.ts",
35
45
  "dev:watch": "node ./scripts/watch.mjs",
46
+ "pretest": "node ./scripts/sync-version.mjs",
36
47
  "test": "vitest run",
37
48
  "test:coverage": "vitest run --coverage",
38
49
  "pretest:coverage": "node ./scripts/pretest-coverage.mjs",
@@ -294,9 +294,9 @@ For frontend workflows, RD and QA must use Playwright MCP (`mcp__playwright__` t
294
294
 
295
295
  ### Workspace initialization gate
296
296
 
297
- The workspace is created in Step 0 (Startup sequence) as a mandatory first action — before any analysis, role handoff, or artifact write, and regardless of how lightweight the request is. Session IDs are now **auto-generated** with the format `YYYY-MM-DD-session-<6位hex>` (e.g. `2026-05-26-session-a3f8b1`). The user does not provide a session ID — the system creates and persists it in `.peaks/.session.json`.
297
+ The workspace is created in Step 0 (Startup sequence) as a mandatory first action — before any analysis, role handoff, or artifact write, and regardless of how lightweight the request is. Session IDs are now **auto-generated** with the format `YYYY-MM-DD-session-<6位hex>` (e.g. `2026-05-26-session-a3f8b1`). The user does not provide a session ID — the system creates and persists it in `.peaks/_runtime/session.json` (the canonical home as of slice `2026-06-05-peaks-runtime-layer`; the legacy `.peaks/.session.json` is read-only back-compat for one minor release).
298
298
 
299
- When `peaks workspace init` is run without `--session-id`, it automatically generates a new session ID using today's date and a random hex suffix. If `.peaks/.session.json` already exists with a valid session, the existing session is reused.
299
+ When `peaks workspace init` is run without `--session-id`, it automatically generates a new session ID using today's date and a random hex suffix. If a valid session binding exists at `.peaks/_runtime/session.json` (or the legacy `.peaks/.session.json` for pre-migration trees), the existing session is reused.
300
300
 
301
301
  **Existing old-session cleanup**: If `.peaks/` contains numeric-only or generic session directories from prior runs (e.g. `2026-05-25-auth-system`), create the new correctly-named session, migrate any reusable artifacts into it, and note the migration in the TXT handoff. Delete empty old-session directories.
302
302
 
@@ -304,9 +304,23 @@ When `peaks workspace init` is run without `--session-id`, it automatically gene
304
304
  peaks workspace init --project <repo> --json
305
305
  ```
306
306
 
307
- The workspace initialization creates this structure under `.peaks/<session-id>/` (where `<session-id>` is auto-generated as `YYYY-MM-DD-session-<6位hex>`):
307
+ The workspace initialization creates this structure under `.peaks/`:
308
308
 
309
309
  ```
310
+ # Canonical home for all per-project ephemeral state (active-skill
311
+ # marker, session binding, sop-state). All writes go here; reads also
312
+ # tolerate the legacy paths (`.peaks/.active-skill.json`,
313
+ # `.peaks/.session.json`, `.peaks/sop-state/`) for one minor release
314
+ # so a fresh upgrade does not break in-flight workflows. Older trees
315
+ # are auto-migrated by `peaks workspace reconcile --apply`.
316
+ .peaks/_runtime/
317
+ ├── active-skill.json # orchestrator presence marker (peaks-solo / -rd / -qa / -ui / -sc / -sop / -txt)
318
+ ├── session.json # project → session binding (the only single-session source of truth)
319
+ └── sop-state/ # current phase + history; definitions live globally in ~/.peaks
320
+
321
+ # Per-slice artifact dirs (auto-generated, one per session). Files
322
+ # inside ARE tracked by the 提交中间产物 convention.
323
+ .peaks/<session-id>/
310
324
  prd/source/ # PRD source documents (Feishu exports, pasted content)
311
325
  prd/requests/ # PRD request artifacts (goals, non-goals, acceptance, frontend delta)
312
326
  ui/requests/ # UI request artifacts (visual direction, taste reports)
@@ -741,6 +755,15 @@ Maintenance: when adding new CLI commands to the runbook, mirror them into both
741
755
 
742
756
  Repair loop details: see `## Mandatory RD QA repair loop` above for the full 5-step procedure and the 3-cycle cap. Append transition notes via `--reason` rather than rewriting artifacts during repair cycles.
743
757
 
758
+ ## RD micro-cycle (TDD small-step rapid-test loop)
759
+
760
+ > **Slice 内部**的修复 / refactor / lint 修复走 micro-cycle(5-10s/cycle)。
761
+ > Slice 边界走 `peaks slice check`(一次性 4 项自检)。
762
+ > 不要把 micro-cycle 跟边界 check 混用——前者 100ms 反馈循环,后者 30s+ 全套。
763
+ > 完整手册:`references/micro-cycle.md`。
764
+ > 摘要:micro-cycle 内只跑 `vitest run <file> -t "<name>"`;边界跑 `peaks slice check`(tsc + vitest + 3-way + verify-pipeline)。
765
+ > 硬约束:违反任一"micro-cycle 内禁止触发"列表 = workflow violation;边界不全绿 = 禁止 ship。
766
+
744
767
  ## Peaks-Cli Project standards preflight
745
768
 
746
769
  Before orchestrating an end-to-end code repository workflow, gather the project standards preflight status from RD and QA by calling the Peaks-Cli CLI:
@@ -791,8 +814,9 @@ These commands harden the workflow against silent skips. Use them in the runbook
791
814
  | `peaks request repair-status <rid> --project <path>` | Count RD↔QA repair cycles from `--reason` transition notes ("QA cycle N: ...") | Before every RD repair iteration in step 7 | Cycle count reached the 3-cycle cap |
792
815
  | `peaks scan request-type-sanity --project <path> --type <type>` | Cross-verify declared `--type` against the actual `git diff` file mix (catches "feature mis-declared as docs" workflow violations) | After PRD type lock-in AND after each RD repair iteration | Declared type disagrees with the file mix |
793
816
  | `peaks scan libraries --project <path>` | Enumerate every dependency + devDependency + peerDependency + optionalDependency with parsed major version; output goes to `## Library versions` in `rd/project-scan.md`. Read-only. | At Solo step 0.6 (alongside `peaks scan archetype`) | Always exits 0 (warnings in JSON envelope; never blocks) |
817
+ | `peaks slice check [--rid <rid>] [--project <path>]` | 4-stage slice 边界 check (typecheck + unit-tests + review-fanout + gate-verify-pipeline). Aggregate pass/fail; non-zero exit if any stage fails. See "Slice 边界 check" below for usage rules (boundary only, never inside a micro-cycle). | At slice 边界(post-micro-cycle, pre-peaks-qa)| Any stage fails |
794
818
 
795
- Together with `peaks request transition` (which already CLI-enforces per-type artifact prerequisites), these four commands form the runtime quality net. SKILL.md prose is descriptive; the CLI is what physically blocks bad workflows.
819
+ Together with `peaks request transition` (which already CLI-enforces per-type artifact prerequisites), these five commands form the runtime quality net. SKILL.md prose is descriptive; the CLI is what physically blocks bad workflows.
796
820
 
797
821
  ## Peaks-Cli Completion handoff
798
822