peaks-cli 1.3.0 → 1.3.2

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/core-artifact-commands.js +49 -11
  4. package/dist/src/cli/commands/hooks-commands.js +24 -9
  5. package/dist/src/cli/commands/progress-commands.js +26 -2
  6. package/dist/src/cli/commands/request-commands.js +5 -0
  7. package/dist/src/cli/commands/slice-commands.d.ts +3 -0
  8. package/dist/src/cli/commands/slice-commands.js +44 -0
  9. package/dist/src/cli/commands/workflow-commands.js +3 -3
  10. package/dist/src/cli/commands/workspace-commands.d.ts +63 -0
  11. package/dist/src/cli/commands/workspace-commands.js +349 -12
  12. package/dist/src/cli/program.js +4 -0
  13. package/dist/src/services/artifacts/artifact-prerequisites.d.ts +29 -1
  14. package/dist/src/services/artifacts/artifact-prerequisites.js +69 -5
  15. package/dist/src/services/artifacts/request-artifact-service.d.ts +22 -0
  16. package/dist/src/services/artifacts/request-artifact-service.js +214 -56
  17. package/dist/src/services/doctor/doctor-service.d.ts +69 -0
  18. package/dist/src/services/doctor/doctor-service.js +296 -3
  19. package/dist/src/services/progress/progress-service.d.ts +26 -0
  20. package/dist/src/services/progress/progress-service.js +25 -0
  21. package/dist/src/services/sc/sc-service.js +71 -13
  22. package/dist/src/services/scan/acceptance-coverage-service.js +6 -2
  23. package/dist/src/services/session/session-manager.d.ts +22 -1
  24. package/dist/src/services/session/session-manager.js +149 -30
  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/slice/slice-check-service.d.ts +2 -0
  28. package/dist/src/services/slice/slice-check-service.js +267 -0
  29. package/dist/src/services/slice/slice-check-types.d.ts +70 -0
  30. package/dist/src/services/slice/slice-check-types.js +18 -0
  31. package/dist/src/services/workflow/pipeline-verify-service.d.ts +5 -2
  32. package/dist/src/services/workflow/pipeline-verify-service.js +35 -35
  33. package/dist/src/services/workspace/migrate-service.d.ts +2 -0
  34. package/dist/src/services/workspace/migrate-service.js +606 -0
  35. package/dist/src/services/workspace/migrate-types.d.ts +127 -0
  36. package/dist/src/services/workspace/migrate-types.js +21 -0
  37. package/dist/src/services/workspace/reconcile-service.d.ts +33 -0
  38. package/dist/src/services/workspace/reconcile-service.js +160 -42
  39. package/dist/src/services/workspace/reconcile-types.d.ts +25 -0
  40. package/dist/src/services/workspace/workspace-service.d.ts +11 -0
  41. package/dist/src/services/workspace/workspace-service.js +71 -24
  42. package/dist/src/shared/change-id.d.ts +59 -0
  43. package/dist/src/shared/change-id.js +194 -16
  44. package/dist/src/shared/version.d.ts +1 -1
  45. package/dist/src/shared/version.js +1 -1
  46. package/package.json +10 -2
  47. package/schemas/doctor-report.schema.json +2 -2
  48. package/skills/peaks-qa/SKILL.md +1 -0
  49. package/skills/peaks-rd/SKILL.md +2 -1
  50. package/skills/peaks-solo/SKILL.md +17 -1
  51. package/skills/peaks-solo/references/micro-cycle.md +155 -0
  52. package/skills/peaks-txt/SKILL.md +2 -0
  53. package/skills/peaks-ui/SKILL.md +1 -0
@@ -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.3.0";
1
+ export declare const CLI_VERSION = "1.3.2";
@@ -1 +1 @@
1
- export const CLI_VERSION = "1.3.0";
1
+ export const CLI_VERSION = "1.3.2";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "peaks-cli",
3
- "version": "1.3.0",
4
- "description": "Peaks CLI and short skill family for Claude Code automation.",
3
+ "version": "1.3.2",
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
  },
@@ -13,8 +13,8 @@
13
13
  "properties": {
14
14
  "id": {
15
15
  "type": "string",
16
- "pattern": "^(skill|skill-name|skill-parse|skill-runbook|skill-apply-note|skill-presence|statusline|schema|config|doctor-self|capability):[A-Za-z0-9][A-Za-z0-9._-]*$",
17
- "description": "Stable check id. Known prefixes: skill:<name> (required skill present), skill-name:<dir> (directory matches declared name), skill-parse:<dir> (skill metadata parsed), skill-runbook:<name> (Default runbook section exists), skill-apply-note:<name> (destructive --apply lines carry an authorization/--dry-run note), skill-presence:<topic> (status of .peaks/.active-skill.json — current/freshness/workspace), statusline:<topic> (out-of-band Claude Code statusLine — install/runtime), schema:<file> (schema file exists and is valid JSON), config:<scope> (optional config locations), doctor-self:<topic> (doctor validates its own output against this schema), capability:<name> (third-party capability is resolvable at the pinned version)."
16
+ "pattern": "^(skill|skill-name|skill-parse|skill-runbook|skill-apply-note|skill-presence|statusline|schema|config|doctor-self|capability|build):[A-Za-z0-9][A-Za-z0-9._-]*$",
17
+ "description": "Stable check id. Known prefixes: skill:<name> (required skill present), skill-name:<dir> (directory matches declared name), skill-parse:<dir> (skill metadata parsed), skill-runbook:<name> (Default runbook section exists), skill-apply-note:<name> (destructive --apply lines carry an authorization/--dry-run note), skill-presence:<topic> (status of .peaks/.active-skill.json — current/freshness/workspace), statusline:<topic> (out-of-band Claude Code statusLine — install/runtime), schema:<file> (schema file exists and is valid JSON), config:<scope> (optional config locations), doctor-self:<topic> (doctor validates its own output against this schema), capability:<name> (third-party capability is resolvable at the pinned version), build:<topic> (build-hygiene checks — dist/source version consistency)."
18
18
  },
19
19
  "ok": { "type": "boolean" },
20
20
  "message": { "type": "string", "minLength": 1 }
@@ -62,6 +62,7 @@ This is the hard-block replacement for the previous "wait for the user" prose. W
62
62
 
63
63
  When this skill is launched as a sub-agent via `Task(subagent_type="general-purpose", ...)` from `peaks-solo`, the following sections of THIS skill are **suspended** for the sub-agent run:
64
64
 
65
+ - **Session id** — use the parent's sid (read `.peaks/_runtime/session.json` or pass `--session-id <parent-sid>` to any session-creating CLI). Do NOT spawn your own session. The new `peaks session info --active` reads the canonical binding for you.
65
66
  - **Skill presence (MANDATORY first action)** — do NOT call `peaks skill presence:set peaks-qa`. The sub-agent must not overwrite `.peaks/.active-skill.json`; the main Solo loop owns that file. If you need to mark your own state, write a marker file at `.peaks/<session-id>/system/sub-agent-qa.json` and only that.
66
67
  - **Workspace initialization** — Solo has already run `peaks workspace init` before fan-out. Do not re-run it.
67
68
  - **Mode selection** — Solo has already chosen the mode.
@@ -39,6 +39,7 @@ The full hard-block contract is defined in `peaks-qa` (see "Hard contracts for b
39
39
 
40
40
  When this skill is launched as a sub-agent via `Task(subagent_type="general-purpose", ...)` from `peaks-solo`, the following sections of THIS skill are **suspended** for the sub-agent run:
41
41
 
42
+ - **Session id** — use the parent's sid (read `.peaks/_runtime/session.json` or pass `--session-id <parent-sid>` to any session-creating CLI). Do NOT spawn your own session. The new `peaks session info --active` reads the canonical binding for you.
42
43
  - **Skill presence (MANDATORY first action)** — do NOT call `peaks skill presence:set peaks-rd`. The sub-agent must not overwrite `.peaks/.active-skill.json`; the main Solo loop owns that file. If you need to mark your own state, write a marker file at `.peaks/<session-id>/system/sub-agent-rd.json` and only that.
43
44
  - **Workspace initialization** — Solo has already run `peaks workspace init` before fan-out. Do not re-run it.
44
45
  - **Mode selection** — Solo has already chosen the mode. Read it from the prompt arguments (or from `.peaks/.active-skill.json` if you can, but do not write it).
@@ -46,7 +47,7 @@ When this skill is launched as a sub-agent via `Task(subagent_type="general-purp
46
47
 
47
48
  What the sub-agent **MUST** still do, from this skill's contract:
48
49
 
49
- 0. **Do NOT call `peaks request init`** — Solo has already initialised the request artefact slot in the main loop before fan-out (the runbook has the exact `peaks request init --role rd --id <rid> --project <repo> --apply --type <type> --json` call). The sub-agent reads the slot via `peaks request show <rid> --role rd --project <repo> --json` if it needs to.
50
+ 0. **Do NOT call `peaks request init`** — Solo has already initialised the request artefact slot in the main loop before fan-out (the runbook has the exact `peaks request init --role rd --id <rid> --project <repo> --apply --type <type> --json` call). The sub-agent reads the slot via `peaks request show <rid> --role rd --project <repo> --json` if it needs to. Note: `peaks request init` is **dry-run by default**. Pass `--apply` to actually create the artifact.
50
51
  2. `peaks request show <rid> --role prd --project <repo> --json` (and `--role ui` if UI is in the swarm plan).
51
52
  3. Standards preflight (dry-run only; Solo owns the apply step).
52
53
  4. Project-scan read; create `rd/project-scan.md` only if Solo flagged it missing in the dispatch prompt.
@@ -262,6 +262,12 @@ peaks session title $(cat .peaks/.session.json | python3 -c "import sys,json; pr
262
262
 
263
263
  If the session directory already has a title (check via `peaks session list --json`), skip this step — the title is already set.
264
264
 
265
+ ## Sub-agent session sharing (MANDATORY — one conversation = one sid)
266
+
267
+ When peaks-solo dispatches a sub-agent (peaks-rd, peaks-qa, peaks-ui, peaks-txt, peaks-sc), the sub-agent prompt MUST include the parent's session id. The sub-agent then passes `--session-id <parent-sid>` for any session-creating CLI call (e.g. `peaks request init --session-id <parent-sid>`). The sub-agent MUST NOT call `peaks workspace init` — that would create a new session dir and orphan the parent's binding. The sub-agent reads `.peaks/_runtime/session.json` to discover the parent's sid (or the orchestrator passes it explicitly). Sub-agents also accept the parent's sid via the new `peaks session info --active` primitive when they need a one-shot read.
268
+
269
+ Note: `peaks request init` is **dry-run by default** — the JSON response has `applied: false` and no file is written unless `--apply` is passed. This is the same safe-by-default pattern as `peaks workspace migrate --apply`. Sub-agents that need to actually create a slice must add `--apply`.
270
+
265
271
  ## Boundaries
266
272
 
267
273
  Peaks-Cli Solo may:
@@ -755,6 +761,15 @@ Maintenance: when adding new CLI commands to the runbook, mirror them into both
755
761
 
756
762
  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.
757
763
 
764
+ ## RD micro-cycle (TDD small-step rapid-test loop)
765
+
766
+ > **Slice 内部**的修复 / refactor / lint 修复走 micro-cycle(5-10s/cycle)。
767
+ > Slice 边界走 `peaks slice check`(一次性 4 项自检)。
768
+ > 不要把 micro-cycle 跟边界 check 混用——前者 100ms 反馈循环,后者 30s+ 全套。
769
+ > 完整手册:`references/micro-cycle.md`。
770
+ > 摘要:micro-cycle 内只跑 `vitest run <file> -t "<name>"`;边界跑 `peaks slice check`(tsc + vitest + 3-way + verify-pipeline)。
771
+ > 硬约束:违反任一"micro-cycle 内禁止触发"列表 = workflow violation;边界不全绿 = 禁止 ship。
772
+
758
773
  ## Peaks-Cli Project standards preflight
759
774
 
760
775
  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:
@@ -805,8 +820,9 @@ These commands harden the workflow against silent skips. Use them in the runbook
805
820
  | `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 |
806
821
  | `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 |
807
822
  | `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) |
823
+ | `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 |
808
824
 
809
- 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.
825
+ 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.
810
826
 
811
827
  ## Peaks-Cli Completion handoff
812
828
 
@@ -0,0 +1,155 @@
1
+ # RD micro-cycle (TDD 小步快测)
2
+
3
+ > 参考 TDD 模式的红绿循环。设计目标:把 1 行 bug fix 的反馈循环从
4
+ > ~30s(全 suite + verify-pipeline)压到 ~100ms(单测 + 心算)。
5
+
6
+ ## 什么时候用 micro-cycle
7
+
8
+ - 在 **RD 实现** 阶段,**slice 内部** 做小修复 / refactor / lint fix / 微调时
9
+ - 节奏:5-10 秒一个 micro-cycle
10
+
11
+ ## 什么时候**不**用 micro-cycle
12
+
13
+ - slice 边界(一个 RD 任务结束 / 用户说 ship / 一个 logical change 整体完成)→ 走 `peaks slice check`
14
+ - 新增 slice / 跨模块 refactor / 依赖升级 → 直接走 peaks-rd 主流程
15
+ - `--type docs` / `--type chore` → 没有 acceptance 表面,micro-cycle 不适用
16
+
17
+ ## The cycle (硬约束顺序)
18
+
19
+ ### 1. RED — 写/改一个 unit test 反映 bug
20
+
21
+ ```bash
22
+ vim tests/unit/<file>.test.ts # 加 1 个 test 反映 bug
23
+ ```
24
+
25
+ 约束:**先写测试,再写实现**。LLM 写完实现再补 test 属于反向 TDD,等同于 skip micro-cycle。
26
+
27
+ ### 2. 跑这一个 test(确认 red)
28
+
29
+ ```bash
30
+ npx vitest run tests/unit/<file>.test.ts \
31
+ -t "<new test name>" \
32
+ --no-coverage
33
+ ```
34
+
35
+ 预期:test FAIL。**如果已经 pass → 你的测试没反映 bug,回去重写**。
36
+
37
+ ### 3. GREEN — 修实现
38
+
39
+ ```bash
40
+ vim src/<file>.ts
41
+ ```
42
+
43
+ 约束:**minimal change**,不要顺手"改进"无关代码。
44
+
45
+ ### 4. 跑这一个 test(确认 green)
46
+
47
+ ```bash
48
+ npx vitest run tests/unit/<file>.test.ts \
49
+ -t "<new test name>" \
50
+ --no-coverage
51
+ ```
52
+
53
+ 预期:test PASS。**如果还 FAIL → 你的实现不对,回去修**。
54
+
55
+ ### 5. 局部回扫 — 跑同 file 的所有 test
56
+
57
+ ```bash
58
+ npx vitest run tests/unit/<file>.test.ts --no-coverage
59
+ ```
60
+
61
+ 目的:防"改一处坏一处"。比全 suite 快 10-50×。
62
+
63
+ ### 6. 写一个 commit message(先不 commit)
64
+
65
+ ```bash
66
+ git add -p
67
+ # commit message: [micro-cycle] <slice-id>: <one-line summary>
68
+ ```
69
+
70
+ ## micro-cycle 内**禁止**触发
71
+
72
+ | 命令 | 理由 |
73
+ |---|---|
74
+ | `npx vitest run`(无 filter)| 30s+,micro-cycle 内禁止 |
75
+ | `npx tsc --noEmit` | 边界点才跑 |
76
+ | `peaks workflow verify-pipeline` | 边界点才跑 |
77
+ | 3-way fan-out(code-review / security-review / perf-baseline)| 边界点 + RD-internal 才跑 |
78
+ | `peaks request transition <rid> --state qa-handoff` | micro-cycle 内**不切 slice 状态** |
79
+
80
+ **违反任何一条 = workflow violation**(slice 边界才能跑全套)。
81
+
82
+ ## 边界 check(slice 结束)
83
+
84
+ 当一个 slice 内的所有 micro-cycle 都 green 且用户/agent 准备进入 peaks-qa 时,**必须**跑:
85
+
86
+ ```bash
87
+ peaks slice check [--rid <rid>] [--project <path>] [--json]
88
+ ```
89
+
90
+ 这个命令编排:
91
+ 1. `npx tsc --noEmit`(typecheck)
92
+ 2. `npx vitest run`(全 suite)
93
+ 3. 3-way fan-out(code-review + security-review + perf-baseline)
94
+ 4. `peaks workflow verify-pipeline --rid <rid> --project <path>`
95
+
96
+ 4 个 check 全绿 + verify-pipeline pass → 才进 `peaks request transition --state qa-handoff`,让 peaks-qa 接管。
97
+
98
+ ## Micro-cycle → 边界 check → QA 的串联
99
+
100
+ ```
101
+ peaks-rd 启动一个 slice
102
+
103
+ bug 1 → micro-cycle (红绿, ~10s)
104
+ bug 2 → micro-cycle
105
+ bug 3 → micro-cycle
106
+ ...
107
+ ↓ 全部 green
108
+ peaks slice check # 4 项检查全绿
109
+
110
+ peaks request transition --state qa-handoff
111
+
112
+ peaks-qa 接管 (full gate machinery)
113
+
114
+ verdict=pass → SC + TXT → handoff
115
+ verdict=return-to-rd → RD 修 (new slice 内部走 micro-cycle)
116
+ ```
117
+
118
+ ## Anti-patterns(明确禁止)
119
+
120
+ - ❌ 写实现先于测试(反向 TDD)
121
+ - ❌ micro-cycle 内跑全 suite(`vitest run`)
122
+ - ❌ micro-cycle 内调 `peaks workflow verify-pipeline`
123
+ - ❌ 1 个 micro-cycle 改 < 1 行代码(合并到下一个相关变更)
124
+ - ❌ skip 边界 check 直接 ship
125
+ - ❌ 在 micro-cycle 内修改 reviewed artifacts(code-review / security-review / perf-baseline)— 等边界再 regenerate
126
+ - ❌ micro-cycle 跨 PR/branch(一次 PR 内的所有 micro-cycles 才合在一起 review)
127
+
128
+ ## 跟其他 skill 的边界
129
+
130
+ | 阶段 | 谁负责 | 节奏 |
131
+ |---|---|---|
132
+ | RD slice 内部 | peaks-solo (main loop) | micro-cycle(5-10s 一个) |
133
+ | RD slice 边界 | peaks-solo 调用 `peaks slice check` | 一次 |
134
+ | QA test execution | peaks-qa (sub-agent or inline) | slice 级 |
135
+ | 3-way fan-out (CR + sec + perf) | peaks-rd (sub-agent) | slice 级(RD 内部一次 + 边界 check 一次) |
136
+ | TXT handoff | peaks-txt | slice 级 |
137
+ | SC commit-boundaries | peaks-sc | slice 级 |
138
+
139
+ ## 为什么这套比当前 peaks-solo 的设计合理
140
+
141
+ - **快**:micro-cycle ~100ms(vs 30s 全 suite),改 10 个 bug 从 5 分钟降到 30 秒
142
+ - **稳**:边界 check 不省,4 项检查(tsc + vitest + 3-way + verify-pipeline)一次全跑
143
+ - **清晰**:LLM 看到一个 explicit "禁止" 列表 + 强制 sequence,比"建议"更不容易越界
144
+ - **可观测**:micro-cycle 走单测 → 边界跑 verify-pipeline,每步都有 JSON envelope 验证
145
+
146
+ ## 跟 peaks-solo SKILL.md 的对账
147
+
148
+ - `peaks slice check` = 边界命令
149
+ - micro-cycle = slice 内部
150
+ - 3-way fan-out = peaks-rd 内部 + `peaks slice check` 末尾
151
+ - `peaks workflow verify-pipeline` = 边界 check
152
+ - `peaks request transition` = 边界切状态
153
+ - peaks-qa 接管 = 边界 + `verdict != pass` 时的下一轮
154
+
155
+ 完整流程见 SKILL.md。
@@ -15,6 +15,8 @@ Before any analysis or tool call, immediately run:
15
15
  peaks skill presence:set peaks-txt --project <repo> --mode <mode> --gate startup
16
16
  ```
17
17
 
18
+ **When invoked as a sub-agent (peaks-solo swarm):** do NOT call `peaks skill presence:set` (Solo owns the active-skill marker) and do NOT spawn your own session. Use the parent's sid — read `.peaks/_runtime/session.json` or pass `--session-id <parent-sid>` to any session-creating CLI. The new `peaks session info --active` reads the canonical binding for you.
19
+
18
20
  On the first presence:set in a project, ensure the out-of-band status bar is installed so the user can see at a glance that Peaks is orchestrating — it renders the active skill in Claude Code's terminal status line, independent of model output:
19
21
 
20
22
  ```bash
@@ -30,6 +30,7 @@ UI's headed-browser inspection hits the same auth walls. The flow is identical t
30
30
 
31
31
  When this skill is launched as a sub-agent via `Task(subagent_type="general-purpose", ...)` from `peaks-solo`, the following sections of THIS skill are **suspended** for the sub-agent run:
32
32
 
33
+ - **Session id** — use the parent's sid (read `.peaks/_runtime/session.json` or pass `--session-id <parent-sid>` to any session-creating CLI). Do NOT spawn your own session. The new `peaks session info --active` reads the canonical binding for you.
33
34
  - **Skill presence (MANDATORY first action)** — do NOT call `peaks skill presence:set peaks-ui`. The sub-agent must not overwrite `.peaks/.active-skill.json`; the main Solo loop owns that file. If you need to mark your own state, write a marker file at `.peaks/<session-id>/system/sub-agent-ui.json` and only that.
34
35
  - **Workspace initialization** — Solo has already run `peaks workspace init` before fan-out. Do not re-run it.
35
36
  - **Mode selection** — Solo has already chosen the mode.