peaks-cli 1.3.5 → 1.3.7
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.
- package/dist/src/cli/commands/slice-commands.js +9 -5
- package/dist/src/cli/commands/workspace-commands.js +46 -2
- package/dist/src/cli/program.js +0 -2
- package/dist/src/services/dashboard/project-dashboard-service.d.ts +0 -7
- package/dist/src/services/dashboard/project-dashboard-service.js +1 -8
- package/dist/src/services/ide/adapters/claude-code-adapter.js +0 -1
- package/dist/src/services/ide/adapters/trae-adapter.js +0 -13
- package/dist/src/services/ide/ide-types.d.ts +0 -2
- package/dist/src/services/session/session-manager.d.ts +55 -0
- package/dist/src/services/session/session-manager.js +68 -0
- package/dist/src/services/skills/skill-presence-service.d.ts +10 -4
- package/dist/src/services/skills/skill-presence-service.js +16 -11
- package/dist/src/services/slice/slice-check-service.js +36 -18
- package/dist/src/services/slice/slice-check-types.d.ts +40 -6
- package/dist/src/services/slice/slice-check-types.js +11 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/skills/peaks-prd/SKILL.md +16 -16
- package/skills/peaks-prd/references/workflow.md +4 -4
- package/skills/peaks-qa/SKILL.md +30 -34
- package/skills/peaks-qa/references/regression-gates.md +1 -1
- package/skills/peaks-rd/SKILL.md +17 -10
- package/skills/peaks-rd/references/{openspec-mcp-cli.md → openspec-cli.md} +11 -14
- package/skills/peaks-solo/SKILL.md +1 -1
- package/skills/peaks-solo/references/a2a-artifact-mapping.md +1 -1
- package/skills/peaks-solo/references/browser-workflow.md +49 -38
- package/skills/peaks-solo/references/external-skill-invocation.md +9 -7
- package/skills/peaks-solo/references/micro-cycle.md +4 -2
- package/skills/peaks-solo/references/{openspec-mcp-workflow.md → openspec-workflow.md} +5 -20
- package/skills/peaks-solo/references/sub-agent-dispatch.md +16 -35
- package/skills/peaks-ui/SKILL.md +22 -24
- package/skills/peaks-ui/references/workflow.md +2 -2
- package/dist/src/cli/commands/mcp-commands.d.ts +0 -3
- package/dist/src/cli/commands/mcp-commands.js +0 -144
- package/dist/src/services/mcp/mcp-apply-service.d.ts +0 -31
- package/dist/src/services/mcp/mcp-apply-service.js +0 -112
- package/dist/src/services/mcp/mcp-call-service.d.ts +0 -17
- package/dist/src/services/mcp/mcp-call-service.js +0 -34
- package/dist/src/services/mcp/mcp-client-service.d.ts +0 -14
- package/dist/src/services/mcp/mcp-client-service.js +0 -49
- package/dist/src/services/mcp/mcp-install-registry.d.ts +0 -11
- package/dist/src/services/mcp/mcp-install-registry.js +0 -38
- package/dist/src/services/mcp/mcp-plan-service.d.ts +0 -29
- package/dist/src/services/mcp/mcp-plan-service.js +0 -109
- package/dist/src/services/mcp/mcp-protocol.d.ts +0 -24
- package/dist/src/services/mcp/mcp-protocol.js +0 -41
- package/dist/src/services/mcp/mcp-scan-service.d.ts +0 -8
- package/dist/src/services/mcp/mcp-scan-service.js +0 -214
- package/dist/src/services/mcp/mcp-stdio-transport.d.ts +0 -10
- package/dist/src/services/mcp/mcp-stdio-transport.js +0 -50
- package/dist/src/services/mcp/mcp-types.d.ts +0 -31
- package/dist/src/services/mcp/mcp-types.js +0 -1
|
@@ -5,25 +5,29 @@ import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
|
5
5
|
export function registerSliceCommands(program, io) {
|
|
6
6
|
const slice = program.command('slice').description('Run slice-level checks (TDD micro-cycle boundary, see ' +
|
|
7
7
|
'skills/peaks-solo/references/micro-cycle.md). `peaks slice check` bundles ' +
|
|
8
|
-
'tsc + vitest + 3-way review fan-out + gate verify-pipeline. ' +
|
|
8
|
+
'tsc + vitest (changed-only by default) + 3-way review fan-out + gate verify-pipeline. ' +
|
|
9
9
|
'Boundaries only; do NOT run inside a micro-cycle.');
|
|
10
10
|
addJsonOption(slice
|
|
11
11
|
.command('check')
|
|
12
12
|
.description('Boundary check for a slice (post-micro-cycle, pre-peaks-qa). ' +
|
|
13
|
-
'Runs 4 stages in order: typecheck → unit-tests
|
|
14
|
-
'
|
|
13
|
+
'Runs 4 stages in order: typecheck → unit-tests (changed-only by default; ' +
|
|
14
|
+
'use --run-tests for the full suite, or --skip-tests to opt out) → ' +
|
|
15
|
+
'review-fanout → gate-verify-pipeline. ' +
|
|
16
|
+
'Each stage reports pass / fail / skipped. ' +
|
|
15
17
|
'Exit 0 only if every stage passes or is skipped.')
|
|
16
18
|
.option('--project <path>', 'target project root', '.')
|
|
17
19
|
.option('--rid <rid>', 'request id; defaults to the active current-change binding')
|
|
18
20
|
.option('--refresh-fanout', 're-run the 3-way review fan-out (peaks-rd) even if the review files already exist', false)
|
|
19
|
-
.option('--
|
|
20
|
-
.option('--
|
|
21
|
+
.option('--run-tests', 'opt in to the FULL test suite at the boundary (default is the changed-only suite via `vitest run --changed`); use the peaks-solo-test skill to run the full suite standalone', false)
|
|
22
|
+
.option('--skip-tests', 'skip the unit-test stage entirely (e.g. docs-only slices); use the peaks-solo-test skill to run the full suite manually if you want a separate check', false)
|
|
23
|
+
.option('--allow-pre-existing-failures', 'opt-in: if the unit-test stage fails, report it as `skipped` with a reason naming the failure count (useful when the repo has unrelated pre-existing failures; the long-term fix is to .skip or coverage.exclude those tests). Only meaningful with --run-tests or the default changed-only mode.', false)).action(async (options) => {
|
|
21
24
|
try {
|
|
22
25
|
const projectRoot = resolveCanonicalProjectRoot(options.project);
|
|
23
26
|
const result = await sliceCheck({
|
|
24
27
|
projectRoot,
|
|
25
28
|
...(options.rid ? { rid: options.rid } : {}),
|
|
26
29
|
refreshFanout: options.refreshFanout === true,
|
|
30
|
+
runTests: options.runTests === true,
|
|
27
31
|
skipTests: options.skipTests === true,
|
|
28
32
|
allowPreExistingFailures: options.allowPreExistingFailures === true
|
|
29
33
|
});
|
|
@@ -4,7 +4,7 @@ import { createInterface } from 'node:readline';
|
|
|
4
4
|
import { initWorkspace, InvalidSessionIdError, ConflictingSessionError } from '../../services/workspace/workspace-service.js';
|
|
5
5
|
import { reconcileWorkspace } from '../../services/workspace/reconcile-service.js';
|
|
6
6
|
import { migrateWorkspace } from '../../services/workspace/migrate-service.js';
|
|
7
|
-
import {
|
|
7
|
+
import { ensureSessionWithRotation } from '../../services/session/session-manager.js';
|
|
8
8
|
import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
|
|
9
9
|
import { applyHookInstall, readHookStatus } from '../../services/skills/hooks-settings-service.js';
|
|
10
10
|
import { fail, ok } from '../../shared/result.js';
|
|
@@ -93,6 +93,7 @@ export function registerWorkspaceCommands(program, io) {
|
|
|
93
93
|
.requiredOption('--project <path>', 'target project root')
|
|
94
94
|
.option('--session-id <id>', 'optional session id in YYYY-MM-DD-<kebab-slug> format. When omitted, the CLI is the single source of truth: an existing binding is reused, otherwise a fresh id is auto-generated.')
|
|
95
95
|
.option('--allow-session-rebind', 'overwrite an existing session binding when the requested session id differs from the project current one', false)
|
|
96
|
+
.option('--no-rotate-on-outer-mismatch', 'suppress the auto-rotation of the project session binding when the outer (Claude / harness) session id has changed. Default rotates on mismatch.')
|
|
96
97
|
.option('--change-id <id>', 'bind the change-id for reviewable artifacts (writes route to .peaks/<change-id>/<role>/, tracked in git). When omitted, the change-id binding is left unchanged.', (value) => {
|
|
97
98
|
if (value.length === 0) {
|
|
98
99
|
throw new Error('--change-id must not be empty');
|
|
@@ -124,12 +125,41 @@ export function registerWorkspaceCommands(program, io) {
|
|
|
124
125
|
// the 5/27-5/29 sessions). When startPath is not inside any
|
|
125
126
|
// git repo, the helper falls through to the cwd verbatim.
|
|
126
127
|
const projectRoot = resolveCanonicalProjectRoot(options.project);
|
|
128
|
+
// Slice 018: outer-session-mismatch auto-rotation. When the
|
|
129
|
+
// user did NOT pass --session-id explicitly, run
|
|
130
|
+
// `ensureSessionWithRotation` so the binding is rotated on
|
|
131
|
+
// outer-mismatch before `initWorkspace` is called. The
|
|
132
|
+
// rotation result is surfaced in the JSON envelope via
|
|
133
|
+
// `data.rotation`. When --session-id IS passed, the user has
|
|
134
|
+
// explicitly told us which session to bind — we honor that
|
|
135
|
+
// verbatim and do NOT rotate (rotation only fires for the
|
|
136
|
+
// auto-detect path).
|
|
127
137
|
let sessionId;
|
|
138
|
+
let rotation = {
|
|
139
|
+
previousSessionId: null,
|
|
140
|
+
reason: null
|
|
141
|
+
};
|
|
128
142
|
if (options.sessionId !== undefined && options.sessionId.length > 0) {
|
|
129
143
|
sessionId = options.sessionId;
|
|
130
144
|
}
|
|
131
145
|
else {
|
|
132
|
-
|
|
146
|
+
const result = await ensureSessionWithRotation(projectRoot, {
|
|
147
|
+
// Commander translates `--no-rotate-on-outer-mismatch` into
|
|
148
|
+
// `options.rotateOnOuterMismatch = false` (the `--no-` prefix
|
|
149
|
+
// is consumed and the remainder becomes the JS property name,
|
|
150
|
+
// with the boolean value flipped). The pre-slice-014 anti-
|
|
151
|
+
// pattern (reading `options.<flag-with-no-prefix> === true`)
|
|
152
|
+
// is NOT used here. The default (no flag) leaves
|
|
153
|
+
// `options.rotateOnOuterMismatch` undefined, which is not
|
|
154
|
+
// equal to `false`, so the default is "rotate on mismatch"
|
|
155
|
+
// (the new auto-roll).
|
|
156
|
+
skipRotateOnOuterMismatch: options.rotateOnOuterMismatch === false
|
|
157
|
+
});
|
|
158
|
+
sessionId = result.sessionId;
|
|
159
|
+
rotation = {
|
|
160
|
+
previousSessionId: result.previousSessionId,
|
|
161
|
+
reason: result.rotationReason
|
|
162
|
+
};
|
|
133
163
|
}
|
|
134
164
|
const report = await initWorkspace({
|
|
135
165
|
projectRoot,
|
|
@@ -141,6 +171,14 @@ export function registerWorkspaceCommands(program, io) {
|
|
|
141
171
|
if (report.previousSessionId !== null && report.bound) {
|
|
142
172
|
nextActions.push(`Replaced prior session binding "${report.previousSessionId}" with "${report.sessionId}".`);
|
|
143
173
|
}
|
|
174
|
+
if (rotation.previousSessionId !== null && rotation.reason === 'outer-session-mismatch') {
|
|
175
|
+
// Outer-session-mismatch rotation: the previous Claude / harness
|
|
176
|
+
// session is no longer the LLM driver. The new binding is fresh,
|
|
177
|
+
// the old session dir is preserved on disk.
|
|
178
|
+
nextActions.push(`Auto-rotated session binding: outer session id changed (was "${rotation.previousSessionId}"). ` +
|
|
179
|
+
`New binding is "${sessionId}". The previous session dir is preserved at .peaks/_runtime/${rotation.previousSessionId}/. ` +
|
|
180
|
+
`Re-run with --no-rotate-on-outer-mismatch to suppress this rotation.`);
|
|
181
|
+
}
|
|
144
182
|
if (report.created.length === 0) {
|
|
145
183
|
nextActions.push('Workspace already initialized — proceed to project scan.');
|
|
146
184
|
}
|
|
@@ -168,6 +206,12 @@ export function registerWorkspaceCommands(program, io) {
|
|
|
168
206
|
}
|
|
169
207
|
printResult(io, ok('workspace.init', {
|
|
170
208
|
...report,
|
|
209
|
+
// Slice 018: surface outer-session-mismatch rotation in the
|
|
210
|
+
// JSON envelope so the LLM and the human both see the swap.
|
|
211
|
+
// Field is omitted (not null) when no rotation fired.
|
|
212
|
+
...(rotation.previousSessionId !== null && rotation.reason !== null
|
|
213
|
+
? { rotation: { previousSessionId: rotation.previousSessionId, reason: rotation.reason } }
|
|
214
|
+
: {}),
|
|
171
215
|
hooksInstall: {
|
|
172
216
|
decision: hooksOutcome.decision,
|
|
173
217
|
action: hooksOutcome.action,
|
package/dist/src/cli/program.js
CHANGED
|
@@ -7,7 +7,6 @@ import { registerCoreAndArtifactCommands } from './commands/core-artifact-comman
|
|
|
7
7
|
import { registerWorkflowCommands } from './commands/workflow-commands.js';
|
|
8
8
|
import { registerCapabilityWorkerConfigAndSCCommands } from './commands/capability-worker-config-sc-commands.js';
|
|
9
9
|
import { registerCodegraphCommands } from './commands/codegraph-commands.js';
|
|
10
|
-
import { registerMcpCommands } from './commands/mcp-commands.js';
|
|
11
10
|
import { registerOpenSpecCommands } from './commands/openspec-commands.js';
|
|
12
11
|
import { registerPerfCommands } from './commands/perf-commands.js';
|
|
13
12
|
// Slice #014: peaks progress * CLI surface deleted (replaced by sub-agent
|
|
@@ -87,7 +86,6 @@ Run peaks (no arguments) for a quickstart. You likely want one of:
|
|
|
87
86
|
registerWorkflowCommands(program, io);
|
|
88
87
|
registerCapabilityWorkerConfigAndSCCommands(program, io);
|
|
89
88
|
registerCodegraphCommands(program, io);
|
|
90
|
-
registerMcpCommands(program, io);
|
|
91
89
|
registerOpenSpecCommands(program, io);
|
|
92
90
|
registerPerfCommands(program, io);
|
|
93
91
|
registerProjectCommands(program, io);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type RequestArtifactRole, type RequestArtifactSummary } from '../artifacts/request-artifact-service.js';
|
|
2
2
|
import type { OpenSpecChangeSummary } from '../openspec/openspec-types.js';
|
|
3
|
-
import type { McpScanReport } from '../mcp/mcp-types.js';
|
|
4
3
|
import type { CapabilityItem } from '../recommendations/recommendation-types.js';
|
|
5
4
|
import { type SkillPresence } from '../skills/skill-presence-service.js';
|
|
6
5
|
export type ProjectDashboardRequests = {
|
|
@@ -19,10 +18,6 @@ export type ProjectDashboardUnderstand = {
|
|
|
19
18
|
graphPath: string;
|
|
20
19
|
parseError?: string;
|
|
21
20
|
};
|
|
22
|
-
export type ProjectDashboardMcp = {
|
|
23
|
-
servers: McpScanReport['servers'];
|
|
24
|
-
scopes: McpScanReport['scopes'];
|
|
25
|
-
};
|
|
26
21
|
export type ProjectDashboardDoctor = {
|
|
27
22
|
ok: boolean;
|
|
28
23
|
passed: number;
|
|
@@ -57,7 +52,6 @@ export type ProjectDashboardRunbookHealth = {
|
|
|
57
52
|
};
|
|
58
53
|
export type ProjectDashboardCapabilities = {
|
|
59
54
|
count: number;
|
|
60
|
-
mcpCount: number;
|
|
61
55
|
sample: Array<Pick<CapabilityItem, 'capabilityId' | 'name' | 'itemType' | 'category'>>;
|
|
62
56
|
};
|
|
63
57
|
export type ProjectDashboardSkillPresence = {
|
|
@@ -76,7 +70,6 @@ export type ProjectDashboard = {
|
|
|
76
70
|
requests: ProjectDashboardRequests;
|
|
77
71
|
openspec: ProjectDashboardOpenSpec;
|
|
78
72
|
understand: ProjectDashboardUnderstand;
|
|
79
|
-
mcp: ProjectDashboardMcp;
|
|
80
73
|
doctor: ProjectDashboardDoctor;
|
|
81
74
|
runbookHealth: ProjectDashboardRunbookHealth;
|
|
82
75
|
capabilities: ProjectDashboardCapabilities;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { listRequestArtifacts } from '../artifacts/request-artifact-service.js';
|
|
2
2
|
import { scanOpenSpec } from '../openspec/openspec-scan-service.js';
|
|
3
|
-
import { scanMcpServers } from '../mcp/mcp-scan-service.js';
|
|
4
3
|
import { scanUnderstandAnything } from '../understand/understand-scan-service.js';
|
|
5
4
|
import { seedCapabilityItems } from '../recommendations/capability-seed-items.js';
|
|
6
5
|
import { requiredSkillNames } from '../../shared/paths.js';
|
|
@@ -78,7 +77,6 @@ function buildCapabilitiesSummary(sampleSize) {
|
|
|
78
77
|
const items = seedCapabilityItems;
|
|
79
78
|
return {
|
|
80
79
|
count: items.length,
|
|
81
|
-
mcpCount: items.filter((item) => item.itemType === 'mcp').length,
|
|
82
80
|
sample: items.slice(0, sampleSize).map((item) => ({
|
|
83
81
|
capabilityId: item.capabilityId,
|
|
84
82
|
name: item.name,
|
|
@@ -109,10 +107,9 @@ export async function loadProjectDashboard(options) {
|
|
|
109
107
|
const clock = options.clock ?? defaultClock;
|
|
110
108
|
const sampleSize = options.sampleCapabilities ?? 8;
|
|
111
109
|
const okPolicy = options.okPolicy ?? 'workspace-only';
|
|
112
|
-
const [items, openspecReport,
|
|
110
|
+
const [items, openspecReport, understandReport, doctorAndRunbook] = await Promise.all([
|
|
113
111
|
listRequestArtifacts({ projectRoot: options.projectRoot }),
|
|
114
112
|
scanOpenSpec({ openspecRoot: `${options.projectRoot}/openspec` }),
|
|
115
|
-
scanMcpServers({ projectRoot: options.projectRoot }),
|
|
116
113
|
scanUnderstandAnything({ projectRoot: options.projectRoot }),
|
|
117
114
|
loadDoctorAndRunbookHealth(options.doctorReport, options.runbookHealth)
|
|
118
115
|
]);
|
|
@@ -142,10 +139,6 @@ export async function loadProjectDashboard(options) {
|
|
|
142
139
|
graphPath: understandReport.graph.path,
|
|
143
140
|
...(understandReport.graph.parseError !== undefined ? { parseError: understandReport.graph.parseError } : {})
|
|
144
141
|
},
|
|
145
|
-
mcp: {
|
|
146
|
-
servers: mcpReport.servers,
|
|
147
|
-
scopes: mcpReport.scopes
|
|
148
|
-
},
|
|
149
142
|
doctor: doctorAndRunbook.doctor,
|
|
150
143
|
runbookHealth: doctorAndRunbook.runbookHealth,
|
|
151
144
|
capabilities: buildCapabilitiesSummary(sampleSize),
|
|
@@ -46,7 +46,6 @@ export const CLAUDE_CODE_ADAPTER = {
|
|
|
46
46
|
capabilities: {
|
|
47
47
|
gateEnforce: true,
|
|
48
48
|
statusline: true,
|
|
49
|
-
mcpInstall: true,
|
|
50
49
|
},
|
|
51
50
|
// Slice #011: standards profile. Claude Code reads its constitution at
|
|
52
51
|
// CLAUDE.md + module-level rules under .claude/rules/**. The values mirror
|
|
@@ -71,19 +71,6 @@ export const TRAE_ADAPTER = {
|
|
|
71
71
|
capabilities: {
|
|
72
72
|
gateEnforce: true,
|
|
73
73
|
statusline: true,
|
|
74
|
-
// Slice #007-007-2026-06-07-mcp-decouple: mcpInstall is LOAD-BEARING.
|
|
75
|
-
// The 4 MCP capabilities (playwright, chrome-devtools, figma, context7)
|
|
76
|
-
// are installed via `peaks mcp plan/apply` which writes to the global
|
|
77
|
-
// `~/.claude/settings.json` file. Trae 1.x's MCP integration is
|
|
78
|
-
// UNVERIFIED (the Trae fixture did not dogfood the MCP install path),
|
|
79
|
-
// so the 6 SKILL.md files must surface a Trae-specific path (manual
|
|
80
|
-
// install + manual tool invocation) rather than promising `peaks mcp
|
|
81
|
-
// apply` will work on Trae. Skill bodies consume this flag through
|
|
82
|
-
// the IDE adapter's `capabilities.mcpInstall`; setting it to true
|
|
83
|
-
// without a real Trae MCP install dogfood would be a regression.
|
|
84
|
-
// Cross-reference: .peaks/memory/trae-adapter-sets-mcpinstall-false-trae-mcp-integration-is-unverified.md
|
|
85
|
-
// and the 4 per-capability memos under .peaks/memory/mcp-decouple-*.md.
|
|
86
|
-
mcpInstall: false
|
|
87
74
|
}
|
|
88
75
|
// Standards: UNVERIFIED — see slice #012+ (Trae real-install dogfood for
|
|
89
76
|
// the `standardsProfile` and `skillInstall` fields). The slice #011
|
|
@@ -18,8 +18,6 @@ export interface IdeCapabilities {
|
|
|
18
18
|
readonly gateEnforce: true;
|
|
19
19
|
/** peaks statusline 状态栏是否适用 */
|
|
20
20
|
readonly statusline: boolean;
|
|
21
|
-
/** peaks mcp install 是否适用 */
|
|
22
|
-
readonly mcpInstall: boolean;
|
|
23
21
|
}
|
|
24
22
|
export interface IdeSettingsLocation {
|
|
25
23
|
/** 项目根下的 settings 目录名,例如 '.claude' / '.trae' / '.cursor' */
|
|
@@ -89,7 +89,62 @@ export declare function setSessionTitle(projectRoot: string, sessionId: string,
|
|
|
89
89
|
* release) but is not authoritative.
|
|
90
90
|
*/
|
|
91
91
|
export declare function listSessionMetas(projectRoot: string): SessionMeta[];
|
|
92
|
+
export type EnsureSessionOptions = {
|
|
93
|
+
/**
|
|
94
|
+
* When `true`, suppress the outer-session-mismatch auto-rotation.
|
|
95
|
+
* The caller wants today's "stamp the field, do not rotate" behaviour
|
|
96
|
+
* even when the outer session id has changed. Used by
|
|
97
|
+
* `peaks workspace init --no-rotate-on-outer-mismatch`.
|
|
98
|
+
*/
|
|
99
|
+
skipRotateOnOuterMismatch?: boolean;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Result of `ensureSessionWithRotation`. When the bound session was
|
|
103
|
+
* rotated because the outer session id had changed, `previousSessionId`
|
|
104
|
+
* is the id of the unbound session and `rotationReason` is the structured
|
|
105
|
+
* reason code the CLI surfaces in its JSON envelope.
|
|
106
|
+
*/
|
|
107
|
+
export type EnsureSessionResult = {
|
|
108
|
+
sessionId: string;
|
|
109
|
+
previousSessionId: string | null;
|
|
110
|
+
rotationReason: 'outer-session-mismatch' | null;
|
|
111
|
+
};
|
|
92
112
|
export declare function ensureSession(projectRoot: string): Promise<string>;
|
|
113
|
+
/**
|
|
114
|
+
* Outer-session-aware wrapper around `ensureSession`.
|
|
115
|
+
*
|
|
116
|
+
* Slice 018 (auto-roll on outer-mismatch). When the current outer
|
|
117
|
+
* session id (sourced from `PEAKS_OUTER_SESSION_ID` with
|
|
118
|
+
* `CLAUDE_CODE_SESSION_ID` as the Claude-Code fallback) differs from
|
|
119
|
+
* the outer session id recorded on the *bound* peaks session's
|
|
120
|
+
* `.peaks/_runtime/<sid>/session.json`, the project-level session
|
|
121
|
+
* binding is rotated before `ensureSession` is called. The old
|
|
122
|
+
* session dir is preserved on disk (data is never wiped) — only the
|
|
123
|
+
* binding changes — and the rotation is surfaced in the return value
|
|
124
|
+
* so the CLI can include it in the JSON envelope.
|
|
125
|
+
*
|
|
126
|
+
* Rotation is suppressed in three cases (all false-positive guards):
|
|
127
|
+
*
|
|
128
|
+
* 1. The current outer session id is undefined (no env var set) —
|
|
129
|
+
* there is no signal to compare against, defaulting to "do not
|
|
130
|
+
* rotate" avoids orphaning the session.
|
|
131
|
+
* 2. The bound session has no recorded `outerSessionId` (legacy
|
|
132
|
+
* session predating the outer-session contract) — there is no
|
|
133
|
+
* signal on the other side either.
|
|
134
|
+
* 3. The bound session's recorded outer session id matches the
|
|
135
|
+
* current one (reconnect within the same Claude session) — this
|
|
136
|
+
* is the common case, not a swap.
|
|
137
|
+
*
|
|
138
|
+
* When `options.skipRotateOnOuterMismatch === true`, the rotation
|
|
139
|
+
* check is short-circuited and the binding is preserved (opt-out for
|
|
140
|
+
* `peaks workspace init --no-rotate-on-outer-mismatch`). The wrapper
|
|
141
|
+
* still delegates to `ensureSession` so the caller gets the existing
|
|
142
|
+
* binding on a reconnect and a fresh id on a first run.
|
|
143
|
+
*
|
|
144
|
+
* Existing public surface is preserved: `ensureSession` is unchanged.
|
|
145
|
+
* This wrapper is the new entry point the CLI uses.
|
|
146
|
+
*/
|
|
147
|
+
export declare function ensureSessionWithRotation(projectRoot: string, options?: EnsureSessionOptions): Promise<EnsureSessionResult>;
|
|
93
148
|
/**
|
|
94
149
|
* Get the current session ID without creating a new one.
|
|
95
150
|
* Returns null if no session exists.
|
|
@@ -421,6 +421,74 @@ export async function ensureSession(projectRoot) {
|
|
|
421
421
|
});
|
|
422
422
|
return sessionId;
|
|
423
423
|
}
|
|
424
|
+
/**
|
|
425
|
+
* Outer-session-aware wrapper around `ensureSession`.
|
|
426
|
+
*
|
|
427
|
+
* Slice 018 (auto-roll on outer-mismatch). When the current outer
|
|
428
|
+
* session id (sourced from `PEAKS_OUTER_SESSION_ID` with
|
|
429
|
+
* `CLAUDE_CODE_SESSION_ID` as the Claude-Code fallback) differs from
|
|
430
|
+
* the outer session id recorded on the *bound* peaks session's
|
|
431
|
+
* `.peaks/_runtime/<sid>/session.json`, the project-level session
|
|
432
|
+
* binding is rotated before `ensureSession` is called. The old
|
|
433
|
+
* session dir is preserved on disk (data is never wiped) — only the
|
|
434
|
+
* binding changes — and the rotation is surfaced in the return value
|
|
435
|
+
* so the CLI can include it in the JSON envelope.
|
|
436
|
+
*
|
|
437
|
+
* Rotation is suppressed in three cases (all false-positive guards):
|
|
438
|
+
*
|
|
439
|
+
* 1. The current outer session id is undefined (no env var set) —
|
|
440
|
+
* there is no signal to compare against, defaulting to "do not
|
|
441
|
+
* rotate" avoids orphaning the session.
|
|
442
|
+
* 2. The bound session has no recorded `outerSessionId` (legacy
|
|
443
|
+
* session predating the outer-session contract) — there is no
|
|
444
|
+
* signal on the other side either.
|
|
445
|
+
* 3. The bound session's recorded outer session id matches the
|
|
446
|
+
* current one (reconnect within the same Claude session) — this
|
|
447
|
+
* is the common case, not a swap.
|
|
448
|
+
*
|
|
449
|
+
* When `options.skipRotateOnOuterMismatch === true`, the rotation
|
|
450
|
+
* check is short-circuited and the binding is preserved (opt-out for
|
|
451
|
+
* `peaks workspace init --no-rotate-on-outer-mismatch`). The wrapper
|
|
452
|
+
* still delegates to `ensureSession` so the caller gets the existing
|
|
453
|
+
* binding on a reconnect and a fresh id on a first run.
|
|
454
|
+
*
|
|
455
|
+
* Existing public surface is preserved: `ensureSession` is unchanged.
|
|
456
|
+
* This wrapper is the new entry point the CLI uses.
|
|
457
|
+
*/
|
|
458
|
+
export async function ensureSessionWithRotation(projectRoot, options) {
|
|
459
|
+
const skipRotate = options?.skipRotateOnOuterMismatch === true;
|
|
460
|
+
const currentOuterSessionId = getCurrentOuterSessionId();
|
|
461
|
+
// Compute the rotation decision up front. We only rotate when ALL
|
|
462
|
+
// three pre-conditions hold: (a) the current outer session id is
|
|
463
|
+
// defined, (b) the bound session has a recorded outer session id,
|
|
464
|
+
// and (c) the two differ. The bound session id is the *first*
|
|
465
|
+
// read so we can use it both for the comparison and for the
|
|
466
|
+
// rotation result.
|
|
467
|
+
const boundSessionId = getSessionId(projectRoot);
|
|
468
|
+
let rotated = null;
|
|
469
|
+
let rotationReason = null;
|
|
470
|
+
if (boundSessionId !== null && currentOuterSessionId !== undefined) {
|
|
471
|
+
const boundMeta = getSessionMeta(projectRoot, boundSessionId);
|
|
472
|
+
const boundOuter = boundMeta?.outerSessionId;
|
|
473
|
+
if (typeof boundOuter === 'string' &&
|
|
474
|
+
boundOuter.length > 0 &&
|
|
475
|
+
boundOuter !== currentOuterSessionId &&
|
|
476
|
+
!skipRotate) {
|
|
477
|
+
rotated = rotateSessionBinding(projectRoot);
|
|
478
|
+
rotationReason = 'outer-session-mismatch';
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
// After the rotation, `ensureSession` will either reuse the
|
|
482
|
+
// canonical-fallback binding (when one still exists, e.g. a sibling
|
|
483
|
+
// projectRoot form) or auto-generate a fresh id. We pass through.
|
|
484
|
+
void rotated; // rotated is the *previous* session id; preserved for the caller via the return value
|
|
485
|
+
const sessionId = await ensureSession(projectRoot);
|
|
486
|
+
return {
|
|
487
|
+
sessionId,
|
|
488
|
+
previousSessionId: rotated,
|
|
489
|
+
rotationReason
|
|
490
|
+
};
|
|
491
|
+
}
|
|
424
492
|
/**
|
|
425
493
|
* Get the current session ID without creating a new one.
|
|
426
494
|
* Returns null if no session exists.
|
|
@@ -22,10 +22,16 @@ export type SkillPresence = {
|
|
|
22
22
|
* Set by `setSkillPresence` when the outer session id changed
|
|
23
23
|
* between the last presence write and this one AND the bound
|
|
24
24
|
* peaks session has a different (or no) recorded outer session id.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
*
|
|
26
|
+
* As of slice 018 (auto-roll on outer-mismatch), the field is
|
|
27
|
+
* informational only — it tells the statusline and any log /
|
|
28
|
+
* observability consumer that an outer-session swap was observed
|
|
29
|
+
* on the previous heartbeat. The actual binding rotation is
|
|
30
|
+
* performed by `ensureSessionWithRotation` (slice 018), not by
|
|
31
|
+
* `setSkillPresence`. `peaks-solo`'s Step 0 used to read this
|
|
32
|
+
* field and turn it into an AskUserQuestion; that ask is no
|
|
33
|
+
* longer needed because the rotation already happened by the time
|
|
34
|
+
* the skill is invoked.
|
|
29
35
|
*/
|
|
30
36
|
outerSessionMismatch?: {
|
|
31
37
|
previous?: string;
|
|
@@ -124,18 +124,23 @@ function getBoundOuterSessionId(projectRootOverride) {
|
|
|
124
124
|
* Used to detect "the LLM just opened a fresh outer session" — if
|
|
125
125
|
* the previously-recorded outer session id differs from the one we
|
|
126
126
|
* are about to stamp, the user probably closed the previous outer
|
|
127
|
-
* session and is now driving peaks from a new one.
|
|
128
|
-
* roll a new peaks session (that is destructive — it would leave
|
|
129
|
-
* the in-flight session with no LLM watching it). Instead we emit
|
|
130
|
-
* a structured `outerSessionMismatch` field on the presence
|
|
131
|
-
* envelope, and peaks-solo's Step 0 turns that into an
|
|
132
|
-
* AskUserQuestion. The user can opt to keep the current session
|
|
133
|
-
* (most common when the swap is a no-op reconnect) or to roll a
|
|
134
|
-
* fresh session (when the new outer session is genuinely a new
|
|
135
|
-
* task).
|
|
127
|
+
* session and is now driving peaks from a new one.
|
|
136
128
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
129
|
+
* As of slice 018 (auto-roll on outer-mismatch), the actual rotation
|
|
130
|
+
* is `ensureSessionWithRotation`'s job, not this one. The presence
|
|
131
|
+
* service still emits the structured `outerSessionMismatch` field on
|
|
132
|
+
* the presence envelope (useful for the statusline to render a stale
|
|
133
|
+
* marker and for the QA / log consumers to know an outer-session swap
|
|
134
|
+
* happened), but it no longer carries the implicit "ask the user"
|
|
135
|
+
* promise — `peaks-solo`'s Step 0 no longer needs to surface an
|
|
136
|
+
* AskUserQuestion, because the rotation already fired by the time the
|
|
137
|
+
* skill is invoked.
|
|
138
|
+
*
|
|
139
|
+
* `getPreviousOuterSessionId` keeps its read-side role: it powers the
|
|
140
|
+
* informational `outerSessionMismatch` field below and the legacy
|
|
141
|
+
* `claudeSessionId` back-compat. Reads from
|
|
142
|
+
* `.peaks/_runtime/active-skill.json` first; falls back to the
|
|
143
|
+
* legacy `.peaks/.active-skill.json` for one minor release.
|
|
139
144
|
*/
|
|
140
145
|
function getPreviousOuterSessionId(projectRootOverride) {
|
|
141
146
|
const result = readSkillPresenceBackCompat(projectRootOverride);
|
|
@@ -69,16 +69,27 @@ function parseVitestSummary(stdout, fallbackDuration) {
|
|
|
69
69
|
durationMs: durationMatch ? Math.round(parseFloat(durationMatch[1]) * 1000) : fallbackDuration
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
-
async function runUnitTests(projectRoot) {
|
|
72
|
+
async function runUnitTests(projectRoot, runTests) {
|
|
73
73
|
const start = Date.now();
|
|
74
|
-
|
|
74
|
+
// Default: changed-only suite (`vitest run --changed`) — runs only tests
|
|
75
|
+
// related to git-changed files. Cost drops from 30s+ to ~1-3s in steady
|
|
76
|
+
// state. Opt-in to the full suite via `runTests: true` (CLI flag
|
|
77
|
+
// `--run-tests`). See `references/runbook.md` for the rationale and
|
|
78
|
+
// `tests/unit/slice-check-service.test.ts` for the regression net.
|
|
79
|
+
const args = runTests
|
|
80
|
+
? ['vitest', 'run', '--reporter=default', '--coverage=false']
|
|
81
|
+
: ['vitest', 'run', '--changed', '--reporter=default', '--coverage=false'];
|
|
82
|
+
const description = runTests
|
|
83
|
+
? 'npx vitest run (full test suite, coverage off)'
|
|
84
|
+
: 'npx vitest run --changed (tests for git-changed files only, coverage off)';
|
|
85
|
+
const result = runCommand('npx', args, projectRoot, 600_000);
|
|
75
86
|
const summary = parseVitestSummary(result.stdout, result.durationMs);
|
|
76
87
|
// Vitest doesn't always print the per-bucket counts cleanly; infer "passed"
|
|
77
88
|
// as total - failed - skipped when failed/skipped buckets are present.
|
|
78
89
|
const passed = Math.max(summary.tests - summary.failed - summary.skipped, 0);
|
|
79
90
|
return {
|
|
80
91
|
name: 'unit-tests',
|
|
81
|
-
description
|
|
92
|
+
description,
|
|
82
93
|
status: result.status,
|
|
83
94
|
durationMs: result.durationMs,
|
|
84
95
|
detail: result.status === 'pass'
|
|
@@ -89,6 +100,7 @@ async function runUnitTests(projectRoot) {
|
|
|
89
100
|
passed,
|
|
90
101
|
failed: summary.failed,
|
|
91
102
|
skipped: summary.skipped,
|
|
103
|
+
mode: runTests ? 'full' : 'changed',
|
|
92
104
|
exitCode: result.exitCode
|
|
93
105
|
}
|
|
94
106
|
};
|
|
@@ -206,40 +218,45 @@ export async function sliceCheck(options) {
|
|
|
206
218
|
}
|
|
207
219
|
const totalStart = Date.now();
|
|
208
220
|
const stages = [];
|
|
221
|
+
let unitTestsRunMode = 'skipped';
|
|
209
222
|
// Stage 1: typecheck
|
|
210
223
|
stages.push(await runTypecheck(options.projectRoot));
|
|
211
|
-
// Stage 2: full
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
224
|
+
// Stage 2: unit-tests — by default changed-only suite, opt-in to full
|
|
225
|
+
if (options.skipTests) {
|
|
226
|
+
stages.push({
|
|
227
|
+
name: 'unit-tests',
|
|
228
|
+
description: 'npx vitest run (skipped per --skip-tests)',
|
|
229
|
+
status: 'skipped',
|
|
230
|
+
durationMs: 0,
|
|
231
|
+
detail: 'Skipped: --skip-tests was set. Use the peaks-solo-test skill to run the full suite manually.'
|
|
232
|
+
});
|
|
233
|
+
unitTestsRunMode = 'skipped';
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
const unitTests = await runUnitTests(options.projectRoot, options.runTests === true);
|
|
215
237
|
// unit-test stage failed, downgrade `failed` to `skipped` with a
|
|
216
238
|
// reason that names the failure count and points to the long-term
|
|
217
|
-
// fix. Does NOT affect the other 3 stages.
|
|
239
|
+
// fix. Does NOT affect the other 3 stages. Only meaningful when
|
|
240
|
+
// the stage actually runs (skipped-tests bypass short-circuits
|
|
241
|
+
// above).
|
|
218
242
|
if (options.allowPreExistingFailures === true &&
|
|
219
243
|
unitTests.status === 'fail') {
|
|
220
244
|
const failureCount = unitTests.data?.failed ?? 0;
|
|
221
245
|
stages.push({
|
|
222
246
|
name: 'unit-tests',
|
|
223
|
-
description:
|
|
247
|
+
description: `npx vitest run ${options.runTests === true ? '' : '--changed '} (overridden via --allow-pre-existing-failures)`.trim(),
|
|
224
248
|
status: 'skipped',
|
|
225
249
|
durationMs: unitTests.durationMs,
|
|
226
250
|
detail: `pre-existing failures: ${failureCount} failing test(s) under coverage.exclude or unrelated to this slice; user opted in via --allow-pre-existing-failures. For the long-term fix, mark these tests .skip or move to coverage.exclude (see dogfood-2-f1-f4.md F17c).`,
|
|
227
251
|
data: { ...(unitTests.data ?? {}), overriddenFrom: 'fail', failureCount }
|
|
228
252
|
});
|
|
253
|
+
unitTestsRunMode = 'overridden';
|
|
229
254
|
}
|
|
230
255
|
else {
|
|
231
256
|
stages.push(unitTests);
|
|
257
|
+
unitTestsRunMode = options.runTests === true ? 'full' : 'changed';
|
|
232
258
|
}
|
|
233
259
|
}
|
|
234
|
-
else {
|
|
235
|
-
stages.push({
|
|
236
|
-
name: 'unit-tests',
|
|
237
|
-
description: 'npx vitest run (skipped per --skip-tests)',
|
|
238
|
-
status: 'skipped',
|
|
239
|
-
durationMs: 0,
|
|
240
|
-
detail: 'Skipped: --skip-tests was set.'
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
260
|
// Stage 3: 3-way review fanout check
|
|
244
261
|
stages.push(await runReviewFanout(options.projectRoot, rid, options.refreshFanout));
|
|
245
262
|
// Stage 4: gate verify-pipeline
|
|
@@ -260,6 +277,7 @@ export async function sliceCheck(options) {
|
|
|
260
277
|
projectRoot: options.projectRoot,
|
|
261
278
|
rid,
|
|
262
279
|
stages,
|
|
280
|
+
unitTestsRunMode,
|
|
263
281
|
boundaryReady,
|
|
264
282
|
totalDurationMs: Date.now() - totalStart,
|
|
265
283
|
nextActions
|
|
@@ -7,13 +7,23 @@
|
|
|
7
7
|
* off to peaks-qa:
|
|
8
8
|
*
|
|
9
9
|
* 1. typecheck (`npx tsc --noEmit`)
|
|
10
|
-
* 2. unit tests
|
|
10
|
+
* 2. unit tests — by default the **changed-only** suite
|
|
11
|
+
* (`npx vitest run --changed`). Pass `--run-tests` to opt in to the
|
|
12
|
+
* full suite (`npx vitest run`); pass `--skip-tests` to skip
|
|
13
|
+
* entirely (e.g. docs-only or config-only slices).
|
|
11
14
|
* 3. 3-way review fan-out (code-review + security-review + perf-baseline)
|
|
12
15
|
* 4. gate machinery (`peaks workflow verify-pipeline --rid <rid>`)
|
|
13
16
|
*
|
|
14
17
|
* The micro-cycle itself (per-bug TDD) runs OUTSIDE slice check — only
|
|
15
18
|
* single-test runs (`vitest -t "<name>"`) are allowed in micro-cycles.
|
|
16
19
|
* This command is for the BOUNDARY, not the inner loop.
|
|
20
|
+
*
|
|
21
|
+
* The unit-test stage emits a `unitTestsRunMode` field on the result
|
|
22
|
+
* envelope so downstream tooling and the QA test-report can record
|
|
23
|
+
* which mode actually ran: `"changed"` (default), `"full"` (with
|
|
24
|
+
* `--run-tests`), `"skipped"` (with `--skip-tests`), or `"overridden"`
|
|
25
|
+
* (with `--allow-pre-existing-failures` when the run failed and the
|
|
26
|
+
* stage was downgraded to `skipped` with a reason).
|
|
17
27
|
*/
|
|
18
28
|
export type SliceCheckStageStatus = 'pass' | 'fail' | 'skipped';
|
|
19
29
|
export type SliceCheckStage = {
|
|
@@ -36,6 +46,15 @@ export type SliceCheckResult = {
|
|
|
36
46
|
rid: string | null;
|
|
37
47
|
/** All stages in execution order. */
|
|
38
48
|
stages: SliceCheckStage[];
|
|
49
|
+
/**
|
|
50
|
+
* Which unit-test mode actually ran. One of:
|
|
51
|
+
* - `"changed"` — default: `npx vitest run --changed` (tests for git-changed files only)
|
|
52
|
+
* - `"full"` — opt-in via `--run-tests`: `npx vitest run` (full suite)
|
|
53
|
+
* - `"skipped"` — opt-in via `--skip-tests` (stage not executed)
|
|
54
|
+
* - `"overridden"` — full mode + `--allow-pre-existing-failures` and the run failed;
|
|
55
|
+
* stage downgraded to `skipped` with the pre-existing-failure reason
|
|
56
|
+
*/
|
|
57
|
+
unitTestsRunMode: 'changed' | 'full' | 'skipped' | 'overridden';
|
|
39
58
|
/** True iff every stage passed (or was skipped) and the boundary is OK to hand off. */
|
|
40
59
|
boundaryReady: boolean;
|
|
41
60
|
/** Total wall-clock duration in ms. */
|
|
@@ -54,17 +73,32 @@ export type SliceCheckOptions = {
|
|
|
54
73
|
*/
|
|
55
74
|
refreshFanout: boolean;
|
|
56
75
|
/**
|
|
57
|
-
* When true,
|
|
58
|
-
*
|
|
76
|
+
* When true, run the **full** `npx vitest run` suite at the boundary.
|
|
77
|
+
* When false (the default), run the **changed-only** suite
|
|
78
|
+
* (`npx vitest run --changed`) which only exercises tests related to
|
|
79
|
+
* git-changed files. The changed-only mode is the new default as of
|
|
80
|
+
* run 017 — full suite costs 30s+ on this repo; the changed-only
|
|
81
|
+
* mode costs ~1-3s in steady state and is what catches the
|
|
82
|
+
* regressions that actually matter. The service treats `undefined`
|
|
83
|
+
* the same as `false`.
|
|
84
|
+
*/
|
|
85
|
+
runTests?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* When true, skip the unit-test stage entirely. Useful when a slice
|
|
88
|
+
* has no test surface (e.g. a docs-only or config-only slice), or
|
|
89
|
+
* when the user wants a "typecheck + review + gate" boundary check
|
|
90
|
+
* without any test execution.
|
|
59
91
|
*/
|
|
60
92
|
skipTests: boolean;
|
|
61
93
|
/**
|
|
62
94
|
* When true, an `unit-tests` stage that fails is reported as `skipped`
|
|
63
95
|
* (with a `reason` naming the pre-existing failure count) instead of
|
|
64
96
|
* `failed`. Used to opt in to bypassing the 28 pre-existing Windows
|
|
65
|
-
* test failures documented in dogfood-2-f1-f4.md F17.
|
|
66
|
-
* the
|
|
67
|
-
*
|
|
97
|
+
* test failures documented in dogfood-2-f1-f4.md F17. Only meaningful
|
|
98
|
+
* when the unit-test stage actually runs (i.e. not when `skipTests`
|
|
99
|
+
* is true). Does NOT affect the other 3 stages (typecheck /
|
|
100
|
+
* review-fanout / gate-verify-pipeline). Default: false. The service
|
|
101
|
+
* treats `undefined` the same as `false`.
|
|
68
102
|
*/
|
|
69
103
|
allowPreExistingFailures?: boolean;
|
|
70
104
|
};
|