peaks-loop 4.0.42 → 4.0.43
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/CHANGELOG.md +27 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/_register.js +4 -0
- package/dist/cli/commands/api-diff-commands.d.ts +16 -0
- package/dist/cli/commands/api-diff-commands.js +55 -0
- package/dist/cli/commands/audit-commands.d.ts +16 -3
- package/dist/cli/commands/audit-commands.js +84 -31
- package/dist/cli/commands/job-commands.js +4 -2
- package/dist/cli/commands/scan-commands.js +1 -1
- package/dist/cli/commands/test-commands.d.ts +60 -3
- package/dist/cli/commands/test-commands.js +125 -7
- package/dist/services/audit/audit-goal-service.js +38 -3
- package/dist/services/doctor/doctor-service/checks/ecc-hooks-schema-drift.d.ts +65 -0
- package/dist/services/doctor/doctor-service/checks/ecc-hooks-schema-drift.js +186 -0
- package/dist/services/doctor/doctor-service/plugin-registry.js +2 -0
- package/dist/services/doctor/doctor-service/types.d.ts +20 -0
- package/dist/services/llm/anthropic-runner.d.ts +87 -0
- package/dist/services/llm/anthropic-runner.js +171 -0
- package/dist/services/llm/stub-runner.d.ts +11 -0
- package/dist/services/llm/stub-runner.js +33 -0
- package/dist/services/prd/project-scan-bootstrap-service.js +7 -7
- package/dist/services/scan/api-diff-openapi.d.ts +32 -0
- package/dist/services/scan/api-diff-openapi.js +359 -0
- package/dist/services/scan/api-diff-recorded.d.ts +96 -0
- package/dist/services/scan/api-diff-recorded.js +577 -0
- package/dist/services/scan/api-diff-service.d.ts +34 -0
- package/dist/services/scan/api-diff-service.js +407 -0
- package/dist/services/scan/api-diff-types.d.ts +116 -0
- package/dist/services/scan/api-diff-types.js +46 -0
- package/dist/services/scan/archetype-service.js +27 -1
- package/dist/services/scan/existing-system-service.js +17 -4
- package/dist/services/scan/hook-convention-service.d.ts +26 -0
- package/dist/services/scan/hook-convention-service.js +562 -0
- package/dist/services/scan/scan-types.d.ts +47 -0
- package/dist/services/session/caller-binding-service.d.ts +28 -0
- package/dist/services/session/caller-binding-service.js +10 -2
- package/dist/services/session/caller-id-types.d.ts +12 -2
- package/dist/services/session/index.d.ts +2 -2
- package/dist/services/session/index.js +2 -2
- package/dist/services/session/session-binding-bridge.js +11 -6
- package/dist/services/session/session-manager.d.ts +33 -1
- package/dist/services/session/session-manager.js +84 -25
- package/dist/services/skills/skill-presence-service.d.ts +17 -3
- package/dist/services/skills/skill-presence-service.js +23 -3
- package/package.json +5 -5
- package/skills/bee/peaks-rd/SKILL.md +11 -3
- package/skills/peaks-code/references/existing-system-extraction.md +5 -1
- package/skills/peaks-code/references/frontend-only-mode.md +48 -6
- package/skills/peaks-code/references/project-scan-checklist.md +20 -1
- package/skills/peaks-doctor/references/doctor-check-catalog.md +1 -0
|
@@ -96,6 +96,15 @@ export function getCallerBinding(projectRoot, callerId) {
|
|
|
96
96
|
return null;
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
+
export function resolveCallerBinding(projectRoot, callerId) {
|
|
100
|
+
const binding = getCallerBinding(projectRoot, callerId);
|
|
101
|
+
if (binding === null)
|
|
102
|
+
return { status: 'absent' };
|
|
103
|
+
if (!existsSync(getSessionDir(projectRoot, binding.peakSessionId))) {
|
|
104
|
+
return { status: 'stale', binding };
|
|
105
|
+
}
|
|
106
|
+
return { status: 'bound', binding };
|
|
107
|
+
}
|
|
99
108
|
/**
|
|
100
109
|
* Write or update a per-caller binding file. The caller is responsible
|
|
101
110
|
* for the binding object (callerId must match the file stem, peakSessionId
|
|
@@ -149,8 +158,7 @@ export function updateCallerBindingSessionId(projectRoot, callerId, peakSessionI
|
|
|
149
158
|
return false;
|
|
150
159
|
setCallerBinding(projectRoot, callerId, {
|
|
151
160
|
...existing,
|
|
152
|
-
peakSessionId
|
|
153
|
-
lastActivityAt: new Date().toISOString()
|
|
161
|
+
peakSessionId
|
|
154
162
|
});
|
|
155
163
|
return true;
|
|
156
164
|
}
|
|
@@ -58,6 +58,18 @@ export interface CallerProjection {
|
|
|
58
58
|
/**
|
|
59
59
|
* On-disk shape of `.peaks/_runtime/callers/<callerId>.json`. One file
|
|
60
60
|
* per caller; two callers may point to the same `peakSessionId` (D6).
|
|
61
|
+
*
|
|
62
|
+
* Slice 2026-09-12 (rid=caller-binding-staleness): the former
|
|
63
|
+
* `lastActivityAt` field is REMOVED. It promised "bumped on every
|
|
64
|
+
* `peaks <cmd>` that touches the binding" but was written only at first
|
|
65
|
+
* bind and at an explicit rebind — never on reuse — so no freshness
|
|
66
|
+
* decision could rest on it without first fixing the write side, and a
|
|
67
|
+
* TTL built on a timestamp that ages on a live binding would un-bind
|
|
68
|
+
* exactly the live sessions the caller-first resolution exists to keep
|
|
69
|
+
* apart. Freshness is decided by the bound session directory (see
|
|
70
|
+
* `resolveCallerBinding`) plus rotation clearing the binding, not by a
|
|
71
|
+
* clock. A legacy file that still carries the key is read fine — the
|
|
72
|
+
* extra property is ignored.
|
|
61
73
|
*/
|
|
62
74
|
export interface CallerBinding {
|
|
63
75
|
/** Echo of the filename stem; matches D1 regex. */
|
|
@@ -68,8 +80,6 @@ export interface CallerBinding {
|
|
|
68
80
|
projectRoot: string;
|
|
69
81
|
/** ISO 8601 timestamp; stamped at first write. */
|
|
70
82
|
createdAt: string;
|
|
71
|
-
/** ISO 8601 timestamp; bumped on every `peaks <cmd>` that touches the binding. */
|
|
72
|
-
lastActivityAt: string;
|
|
73
83
|
/** Last skill that touched this binding, e.g. "peaks-code". */
|
|
74
84
|
skill: string;
|
|
75
85
|
/** Last mode, e.g. "full-auto". */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, setCurrentSessionBinding, rotateSessionBinding, type SessionInfo, type SessionMeta } from './session-manager.js';
|
|
1
|
+
export { ensureSession, getSessionId, resolveCallerBoundSession, type CallerBoundSession, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, setCurrentSessionBinding, rotateSessionBinding, type SessionInfo, type SessionMeta } from './session-manager.js';
|
|
2
2
|
export { getSessionDir } from './getSessionDir.js';
|
|
3
3
|
export { resolveCallerId, } from './resolve-caller-id.js';
|
|
4
|
-
export { getCallerBindingFile, getActiveSkillFileForCaller, synthesiseLegacyCallerId, getCallerBinding, setCallerBinding, listCallerBindings } from './caller-binding-service.js';
|
|
4
|
+
export { getCallerBindingFile, getActiveSkillFileForCaller, synthesiseLegacyCallerId, getCallerBinding, resolveCallerBinding, type CallerBindingResolution, setCallerBinding, listCallerBindings } from './caller-binding-service.js';
|
|
5
5
|
export { PLATFORM_FALLBACKS, type PlatformFallback } from './platform-fallbacks.js';
|
|
6
6
|
export { resolveCallerProjection, type ResolveCallerIdOptions } from './resolve-caller-id.js';
|
|
7
7
|
export { type CallerProjection, type CallerProjectionSource, type CallerResolveErrorCode } from './caller-id-types.js';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, setCurrentSessionBinding, rotateSessionBinding } from './session-manager.js';
|
|
1
|
+
export { ensureSession, getSessionId, resolveCallerBoundSession, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, setCurrentSessionBinding, rotateSessionBinding } from './session-manager.js';
|
|
2
2
|
export { getSessionDir } from './getSessionDir.js';
|
|
3
3
|
// Slice 020 — caller-keyed session binding. The new canonical path.
|
|
4
4
|
export { resolveCallerId, } from './resolve-caller-id.js';
|
|
5
|
-
export { getCallerBindingFile, getActiveSkillFileForCaller, synthesiseLegacyCallerId, getCallerBinding, setCallerBinding, listCallerBindings } from './caller-binding-service.js';
|
|
5
|
+
export { getCallerBindingFile, getActiveSkillFileForCaller, synthesiseLegacyCallerId, getCallerBinding, resolveCallerBinding, setCallerBinding, listCallerBindings } from './caller-binding-service.js';
|
|
6
6
|
// Slice 4.0.8 (C1): PLATFORM_FALLBACKS is deleted. Re-export kept as
|
|
7
7
|
// a deprecated alias for one minor release so legacy consumers keep
|
|
8
8
|
// type-checking; the array is empty and unused at runtime. New code
|
|
@@ -23,7 +23,7 @@ import { dirname, join } from 'node:path';
|
|
|
23
23
|
import { randomBytes } from 'node:crypto';
|
|
24
24
|
import { initWorkspace } from '../workspace/workspace-service.js';
|
|
25
25
|
import { projectRootsMatch, stableRealPath } from '../../shared/path-utils.js';
|
|
26
|
-
import {
|
|
26
|
+
import { resolveCallerBinding, setCallerBinding, } from './caller-binding-service.js';
|
|
27
27
|
import { resolveCallerProjection } from './resolve-caller-id.js';
|
|
28
28
|
import { getSessionId, getSessionIdCanonical, getSessionMeta, rotateSessionBinding, setSessionMeta } from './session-manager.js';
|
|
29
29
|
// --- Lower-level helpers the bridge needs (moved verbatim) ---
|
|
@@ -205,6 +205,12 @@ export function _resetLastResolvedOuterForTest() {
|
|
|
205
205
|
* `ensureSession` consults it BEFORE `readSessionFile` so a binding
|
|
206
206
|
* written by a previous call from the same caller is preferred over
|
|
207
207
|
* a stale `session.json` (the legacy single-file binding).
|
|
208
|
+
*
|
|
209
|
+
* Slice 2026-09-12 (rid=caller-binding-staleness): the binding is only
|
|
210
|
+
* usable when its bound session directory still exists
|
|
211
|
+
* (`resolveCallerBinding`). A binding pointing at a deleted session
|
|
212
|
+
* directory is treated as absent, so `ensureSession` falls through and
|
|
213
|
+
* rolls a fresh session instead of returning an id whose tree is gone.
|
|
208
214
|
*/
|
|
209
215
|
function resolveCallerBindingForEnsure(projectRoot) {
|
|
210
216
|
let projection;
|
|
@@ -215,12 +221,12 @@ function resolveCallerBindingForEnsure(projectRoot) {
|
|
|
215
221
|
return null;
|
|
216
222
|
}
|
|
217
223
|
try {
|
|
218
|
-
const
|
|
219
|
-
if (
|
|
224
|
+
const resolution = resolveCallerBinding(projectRoot, projection.callerId);
|
|
225
|
+
if (resolution.status !== 'bound')
|
|
220
226
|
return null;
|
|
221
227
|
return {
|
|
222
|
-
sessionId: binding.peakSessionId,
|
|
223
|
-
createdAt: binding.createdAt,
|
|
228
|
+
sessionId: resolution.binding.peakSessionId,
|
|
229
|
+
createdAt: resolution.binding.createdAt,
|
|
224
230
|
callerId: projection.callerId
|
|
225
231
|
};
|
|
226
232
|
}
|
|
@@ -324,7 +330,6 @@ export async function ensureSession(projectRoot) {
|
|
|
324
330
|
peakSessionId: sessionId,
|
|
325
331
|
projectRoot,
|
|
326
332
|
createdAt: now,
|
|
327
|
-
lastActivityAt: now,
|
|
328
333
|
skill: 'peaks-code',
|
|
329
334
|
mode: 'unknown',
|
|
330
335
|
gate: 'startup'
|
|
@@ -39,7 +39,10 @@ export type SessionMeta = {
|
|
|
39
39
|
* is left intact — rotating does NOT delete the user's data, it
|
|
40
40
|
* just unbinds the project from that session. Also drops the legacy
|
|
41
41
|
* `.peaks/.session.json` if present so a stale read from another
|
|
42
|
-
* tool cannot re-bind the project after rotation.
|
|
42
|
+
* tool cannot re-bind the project after rotation. Also drops the
|
|
43
|
+
* ROTATING caller's per-caller binding when it points at the rotated-out
|
|
44
|
+
* session (slice 2026-09-12, rid=caller-binding-staleness) — see
|
|
45
|
+
* `clearRotatedCallerBinding`.
|
|
43
46
|
*
|
|
44
47
|
* Returns the id of the session that was unbound, or `null` if
|
|
45
48
|
* no binding was present. The caller is expected to do something
|
|
@@ -126,6 +129,35 @@ export declare function getSessionId(projectRoot: string): string | null;
|
|
|
126
129
|
* so this variant is opt-in.
|
|
127
130
|
*/
|
|
128
131
|
export declare function getSessionIdCanonical(projectRoot: string): string | null;
|
|
132
|
+
/**
|
|
133
|
+
* Outcome of the caller-binding half of session resolution.
|
|
134
|
+
*
|
|
135
|
+
* `staleSessionId` is the id of a per-caller binding that was SKIPPED
|
|
136
|
+
* because its session directory no longer exists — `null` for the
|
|
137
|
+
* ordinary "no binding" case. It exists so the fall-through is
|
|
138
|
+
* observable: a caller can tell "this caller has no binding" apart from
|
|
139
|
+
* "this caller's binding pointed at a session tree that is gone", and
|
|
140
|
+
* report it instead of silently resolving a different session.
|
|
141
|
+
*/
|
|
142
|
+
export type CallerBoundSession = {
|
|
143
|
+
sessionId: string | null;
|
|
144
|
+
staleSessionId: string | null;
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Resolve the caller-binding primary source (slice 2026-09-12,
|
|
148
|
+
* rid=caller-binding-staleness).
|
|
149
|
+
*
|
|
150
|
+
* The caller-first precedence is unchanged: this caller's binding still
|
|
151
|
+
* outranks the project-global `session.json`. What changed is what counts
|
|
152
|
+
* as a USABLE binding — a binding whose bound session directory is gone
|
|
153
|
+
* is stale, is not trusted, and is reported through `staleSessionId`
|
|
154
|
+
* instead of being returned as if it were live.
|
|
155
|
+
*
|
|
156
|
+
* Returns `{ sessionId: null, staleSessionId: null }` when caller-id
|
|
157
|
+
* resolution fails (`PEAKS_CALLER_NOT_RESOLVED`), when no per-caller file
|
|
158
|
+
* exists, or when the file is malformed.
|
|
159
|
+
*/
|
|
160
|
+
export declare function resolveCallerBoundSession(projectRoot: string): CallerBoundSession;
|
|
129
161
|
/**
|
|
130
162
|
* Get the absolute path to the current session directory.
|
|
131
163
|
* Creates the session if it doesn't exist.
|
|
@@ -11,7 +11,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
11
11
|
import { randomBytes } from 'node:crypto';
|
|
12
12
|
import { projectRootsMatch, stableRealPath } from '../../shared/path-utils.js';
|
|
13
13
|
import { ensureSession } from './session-binding-bridge.js';
|
|
14
|
-
import { getCallerBinding } from './caller-binding-service.js';
|
|
14
|
+
import { getCallerBinding, getCallerBindingFile, resolveCallerBinding } from './caller-binding-service.js';
|
|
15
15
|
import { resolveCallerProjection } from './resolve-caller-id.js';
|
|
16
16
|
// As of slice 2026-06-05-peaks-runtime-layer the project-level session
|
|
17
17
|
// binding lives under `.peaks/_runtime/session.json`. The legacy
|
|
@@ -199,7 +199,10 @@ function writeSessionFile(projectRoot, info) {
|
|
|
199
199
|
* is left intact — rotating does NOT delete the user's data, it
|
|
200
200
|
* just unbinds the project from that session. Also drops the legacy
|
|
201
201
|
* `.peaks/.session.json` if present so a stale read from another
|
|
202
|
-
* tool cannot re-bind the project after rotation.
|
|
202
|
+
* tool cannot re-bind the project after rotation. Also drops the
|
|
203
|
+
* ROTATING caller's per-caller binding when it points at the rotated-out
|
|
204
|
+
* session (slice 2026-09-12, rid=caller-binding-staleness) — see
|
|
205
|
+
* `clearRotatedCallerBinding`.
|
|
203
206
|
*
|
|
204
207
|
* Returns the id of the session that was unbound, or `null` if
|
|
205
208
|
* no binding was present. The caller is expected to do something
|
|
@@ -231,8 +234,49 @@ export function rotateSessionBinding(projectRoot) {
|
|
|
231
234
|
// best-effort: a stale legacy binding is not blocking
|
|
232
235
|
}
|
|
233
236
|
}
|
|
237
|
+
// Slice 2026-09-12 (rid=caller-binding-staleness): the per-caller
|
|
238
|
+
// binding is the FIRST thing `getSessionId` reads, so unlinking only
|
|
239
|
+
// the project-global files left the rotating caller still resolving
|
|
240
|
+
// the session it just rotated out of — `peaks job init` re-created
|
|
241
|
+
// `.peaks/_runtime/<old-sid>/job/...` instead of reporting
|
|
242
|
+
// NO_ACTIVE_SESSION, and `ensureSession` returned the old id instead
|
|
243
|
+
// of rolling a fresh session. The rotation is only complete once THIS
|
|
244
|
+
// caller's binding is gone too (the next resolution then either
|
|
245
|
+
// falls through to the global file or starts a fresh session).
|
|
246
|
+
clearRotatedCallerBinding(projectRoot, previous.sessionId);
|
|
234
247
|
return previous.sessionId;
|
|
235
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Drop the ROTATING caller's per-caller binding when it points at the
|
|
251
|
+
* session being rotated out.
|
|
252
|
+
*
|
|
253
|
+
* Only that caller is affected: every other caller holds its own session
|
|
254
|
+
* on purpose (D6 multi-caller isolation — a second IDE window must not
|
|
255
|
+
* lose its binding because a sibling window rotated). When no caller id
|
|
256
|
+
* is resolvable (CI / plain shell), there is no per-caller binding to
|
|
257
|
+
* clear and this is a no-op.
|
|
258
|
+
*/
|
|
259
|
+
function clearRotatedCallerBinding(projectRoot, rotatedOutSessionId) {
|
|
260
|
+
let callerId;
|
|
261
|
+
try {
|
|
262
|
+
callerId = resolveCallerProjection({ projectRoot, env: process.env }).callerId;
|
|
263
|
+
}
|
|
264
|
+
catch { // PEAKS_CALLER_NOT_RESOLVED → no per-caller binding exists
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const binding = getCallerBinding(projectRoot, callerId);
|
|
268
|
+
if (binding === null || binding.peakSessionId !== rotatedOutSessionId) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
unlinkSync(getCallerBindingFile(projectRoot, callerId));
|
|
273
|
+
}
|
|
274
|
+
catch { // TODO(g2): best-effort unlink — rotation must not fail on a cleanup
|
|
275
|
+
// error (Windows EPERM). On failure the binding survives; it is stale
|
|
276
|
+
// for as long as its session directory is gone, and the next rotation
|
|
277
|
+
// or rebind retries the clear.
|
|
278
|
+
}
|
|
279
|
+
}
|
|
236
280
|
/**
|
|
237
281
|
* Bind the project's current session to the given session id by writing
|
|
238
282
|
* `.peaks/.session.json`. The single-session binding is the source of truth
|
|
@@ -401,17 +445,19 @@ export { ensureSession, ensureSessionWithRotation } from './session-binding-brid
|
|
|
401
445
|
*/
|
|
402
446
|
export function getSessionId(projectRoot) {
|
|
403
447
|
// Slice 2026-08-06-session-cacde8-A.5a: caller-binding becomes
|
|
404
|
-
// primary source. Read order is (1)
|
|
405
|
-
// `resolveCallerProjection` succeeds
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
448
|
+
// primary source. Read order is (1) the per-caller binding if
|
|
449
|
+
// `resolveCallerProjection` succeeds — and only when that binding is
|
|
450
|
+
// still usable (`resolveCallerBoundSession` drops one whose session
|
|
451
|
+
// directory is gone, slice 2026-09-12 rid=caller-binding-staleness),
|
|
452
|
+
// (2) `readSessionFile` (the legacy session.json), (3) null (no
|
|
453
|
+
// binding). The legacy session.json is preserved as a fallback so a
|
|
454
|
+
// project without a caller-id resolution still resolves its binding
|
|
455
|
+
// (e.g. CLI run from a stock shell with no IDE adapter).
|
|
456
|
+
// `PEAKS_CALLER_NOT_RESOLVED` falls through to the session.json path;
|
|
457
|
+
// no caller-facing exit change.
|
|
458
|
+
const { sessionId } = resolveCallerBoundSession(projectRoot);
|
|
459
|
+
if (sessionId !== null)
|
|
460
|
+
return sessionId;
|
|
415
461
|
const info = readSessionFile(projectRoot);
|
|
416
462
|
return info?.sessionId ?? null;
|
|
417
463
|
}
|
|
@@ -446,27 +492,40 @@ export function getSessionIdCanonical(projectRoot) {
|
|
|
446
492
|
// Slice 2026-08-06-session-cacde8-A.5a: same caller-binding primary
|
|
447
493
|
// lookup as `getSessionId`; fall back to the canonical-fallback
|
|
448
494
|
// `readSessionFileCanonical` if caller-binding is absent / unresolved.
|
|
449
|
-
const
|
|
450
|
-
if (
|
|
451
|
-
return
|
|
495
|
+
const { sessionId } = resolveCallerBoundSession(projectRoot);
|
|
496
|
+
if (sessionId !== null)
|
|
497
|
+
return sessionId;
|
|
452
498
|
const info = readSessionFileCanonical(projectRoot);
|
|
453
499
|
return info?.sessionId ?? null;
|
|
454
500
|
}
|
|
455
501
|
/**
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
502
|
+
* Resolve the caller-binding primary source (slice 2026-09-12,
|
|
503
|
+
* rid=caller-binding-staleness).
|
|
504
|
+
*
|
|
505
|
+
* The caller-first precedence is unchanged: this caller's binding still
|
|
506
|
+
* outranks the project-global `session.json`. What changed is what counts
|
|
507
|
+
* as a USABLE binding — a binding whose bound session directory is gone
|
|
508
|
+
* is stale, is not trusted, and is reported through `staleSessionId`
|
|
509
|
+
* instead of being returned as if it were live.
|
|
510
|
+
*
|
|
511
|
+
* Returns `{ sessionId: null, staleSessionId: null }` when caller-id
|
|
512
|
+
* resolution fails (`PEAKS_CALLER_NOT_RESOLVED`), when no per-caller file
|
|
513
|
+
* exists, or when the file is malformed.
|
|
461
514
|
*/
|
|
462
|
-
function
|
|
515
|
+
export function resolveCallerBoundSession(projectRoot) {
|
|
463
516
|
try {
|
|
464
517
|
const projection = resolveCallerProjection({ projectRoot, env: process.env });
|
|
465
|
-
const
|
|
466
|
-
|
|
518
|
+
const resolution = resolveCallerBinding(projectRoot, projection.callerId);
|
|
519
|
+
if (resolution.status === 'bound') {
|
|
520
|
+
return { sessionId: resolution.binding.peakSessionId, staleSessionId: null };
|
|
521
|
+
}
|
|
522
|
+
if (resolution.status === 'stale') {
|
|
523
|
+
return { sessionId: null, staleSessionId: resolution.binding.peakSessionId };
|
|
524
|
+
}
|
|
525
|
+
return { sessionId: null, staleSessionId: null };
|
|
467
526
|
}
|
|
468
527
|
catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
|
|
469
|
-
return null;
|
|
528
|
+
return { sessionId: null, staleSessionId: null };
|
|
470
529
|
}
|
|
471
530
|
}
|
|
472
531
|
/**
|
|
@@ -52,9 +52,23 @@ export type SkillPresence = {
|
|
|
52
52
|
lastHeartbeat?: string;
|
|
53
53
|
};
|
|
54
54
|
/**
|
|
55
|
-
* Resolve the active peaks session id
|
|
56
|
-
*
|
|
57
|
-
*
|
|
55
|
+
* Resolve the active peaks session id.
|
|
56
|
+
*
|
|
57
|
+
* Resolution order (slice caller-first-session-resolution):
|
|
58
|
+
* 1. The per-caller binding (via `getSessionId`) — the SAME source
|
|
59
|
+
* `peaks session info --active` resolves through.
|
|
60
|
+
* 2. The project-global `.peaks/_runtime/session.json` (legacy:
|
|
61
|
+
* `.peaks/.session.json`) — unchanged back-compat fallback.
|
|
62
|
+
* 3. `null` when neither is present.
|
|
63
|
+
*
|
|
64
|
+
* The caller-first step is what keeps two IDE windows on one project
|
|
65
|
+
* apart. The project-global file is last-writer-wins, so the window
|
|
66
|
+
* that initialised most recently owned it for every reader; commands
|
|
67
|
+
* that resolved through this helper (job / dispatch / worktree /
|
|
68
|
+
* share / ...) then wrote into THAT window's session tree, silently
|
|
69
|
+
* cross-contaminating the two sessions.
|
|
70
|
+
*
|
|
71
|
+
* Returns `null` when no session is bound. Never throws.
|
|
58
72
|
*
|
|
59
73
|
* Public export: callers like `peaks sub-agent dispatch` use this
|
|
60
74
|
* to auto-resolve `--session-id` when the LLM driver forgets to
|
|
@@ -81,9 +81,23 @@ function resolveProjectRoot(override) {
|
|
|
81
81
|
return findProjectRoot(process.cwd()) ?? process.cwd();
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
-
* Resolve the active peaks session id
|
|
85
|
-
*
|
|
86
|
-
*
|
|
84
|
+
* Resolve the active peaks session id.
|
|
85
|
+
*
|
|
86
|
+
* Resolution order (slice caller-first-session-resolution):
|
|
87
|
+
* 1. The per-caller binding (via `getSessionId`) — the SAME source
|
|
88
|
+
* `peaks session info --active` resolves through.
|
|
89
|
+
* 2. The project-global `.peaks/_runtime/session.json` (legacy:
|
|
90
|
+
* `.peaks/.session.json`) — unchanged back-compat fallback.
|
|
91
|
+
* 3. `null` when neither is present.
|
|
92
|
+
*
|
|
93
|
+
* The caller-first step is what keeps two IDE windows on one project
|
|
94
|
+
* apart. The project-global file is last-writer-wins, so the window
|
|
95
|
+
* that initialised most recently owned it for every reader; commands
|
|
96
|
+
* that resolved through this helper (job / dispatch / worktree /
|
|
97
|
+
* share / ...) then wrote into THAT window's session tree, silently
|
|
98
|
+
* cross-contaminating the two sessions.
|
|
99
|
+
*
|
|
100
|
+
* Returns `null` when no session is bound. Never throws.
|
|
87
101
|
*
|
|
88
102
|
* Public export: callers like `peaks sub-agent dispatch` use this
|
|
89
103
|
* to auto-resolve `--session-id` when the LLM driver forgets to
|
|
@@ -93,6 +107,12 @@ function resolveProjectRoot(override) {
|
|
|
93
107
|
*/
|
|
94
108
|
export function getCurrentSessionId(projectRootOverride) {
|
|
95
109
|
const projectRoot = resolveProjectRoot(projectRootOverride);
|
|
110
|
+
// Caller-first: a binding for THIS caller outranks the project-global
|
|
111
|
+
// file. Delegated to `getSessionId` rather than re-implemented, so the
|
|
112
|
+
// two answers to "which session is current?" cannot drift again.
|
|
113
|
+
const callerBound = getSessionId(projectRoot);
|
|
114
|
+
if (callerBound !== null)
|
|
115
|
+
return callerBound;
|
|
96
116
|
const sessionPath = resolve(projectRoot, SESSION_FILE);
|
|
97
117
|
const legacyPath = resolve(projectRoot, SESSION_FILE_LEGACY);
|
|
98
118
|
// Back-compat window: prefer the new canonical path; fall back to the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-loop",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.43",
|
|
4
4
|
"description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
|
|
5
5
|
"author": "SquabbyZ",
|
|
6
6
|
"keywords": [
|
|
@@ -101,10 +101,10 @@
|
|
|
101
101
|
"fzf": "^0.5.2",
|
|
102
102
|
"yaml": "^2.9.0",
|
|
103
103
|
"zod": "^4.4.3",
|
|
104
|
-
"peaks-loop-
|
|
105
|
-
"peaks-loop-shared": "0.0.
|
|
106
|
-
"peaks-loop-
|
|
107
|
-
"peaks-loop-shared-channel": "0.0.
|
|
104
|
+
"peaks-loop-mut": "0.1.41",
|
|
105
|
+
"peaks-loop-shared": "0.0.77",
|
|
106
|
+
"peaks-loop-internal-runtime": "0.0.28",
|
|
107
|
+
"peaks-loop-shared-channel": "0.0.45"
|
|
108
108
|
},
|
|
109
109
|
"devDependencies": {
|
|
110
110
|
"@changesets/cli": "2.31.1",
|
|
@@ -223,11 +223,19 @@ When RD work creates a frontend application and the user has not specified a tec
|
|
|
223
223
|
|
|
224
224
|
→ see `references/frontend-project-generation.md` for the scaffold protocol.
|
|
225
225
|
|
|
226
|
-
## Frontend anti-corruption layer (ACL)
|
|
226
|
+
## Frontend anti-corruption layer (ACL) — route by integration mode first
|
|
227
227
|
|
|
228
|
-
When RD work touches the frontend
|
|
228
|
+
When RD work touches the frontend, read `.integrationMode` from `## Project mode` in `.peaks/project-scan/project-scan.md` (written from `peaks scan archetype --json`) and run that mode's procedure. Do **not** route on the `frontendOnly` boolean: it is back-compat only and merges two modes that need different first commands (`frontendOnly=false` covers both `full-stack` and `prd-plus-interface-doc`; `frontendOnly=true` covers `prd-only`).
|
|
229
229
|
|
|
230
|
-
|
|
230
|
+
| `.integrationMode` | RD runs first | Contract source | Artifact that proves it ran |
|
|
231
|
+
|---|---|---|---|
|
|
232
|
+
| `full-stack` | read `## API` in `.peaks/project-scan/project-scan.md` (a real wrapper + endpoint inventory exist here) | the backend's own response type in this repo | the per-domain mapper at the path in `## API → DTO ↔ ViewModel boundary`, plus `peaks scan diff-vs-scope --rid <rid> --project <repo>` |
|
|
233
|
+
| `prd-plus-interface-doc` | `peaks scan api-diff <doc> --project <repo>`, **before writing any type** | the OpenAPI document | the doc-derived `src/services/types/<feature>-api.types.ts` + the `Exact` section recorded in `.peaks/_runtime/<sessionId>/rd/requests/<rid>.md` |
|
|
234
|
+
| `prd-only` | write `.peaks/_runtime/<sessionId>/rd/mock-plan.md` before any mock file | an authored guess | `mock-plan.md` + `mock/<feature>-mock.ts` |
|
|
235
|
+
|
|
236
|
+
Whichever the mode, the mapping rule is unchanged: the frontend's internal model is never polluted by external / API shapes, DTO ↔ ViewModel conversion happens at the boundary, and all conversion logic for a domain lives in exactly one mapper file (e.g. `mappers/user.mapper.ts`) — never scattered across pages. Hard constraint, verified by QA / code-review.
|
|
237
|
+
|
|
238
|
+
→ see `references/frontend-acl-mapper.md` for the three rules + reference shape + verification, and `skills/peaks-code/references/frontend-only-mode.md` §"Integration-mode routing (RD)" for the numbered procedure per mode — including the two checks each mode **cannot** perform (doc↔server drift; and, for `prd-only`, that no mechanism detects a real document appearing, since the §2.6 staleness artifact is not built).
|
|
231
239
|
|
|
232
240
|
## Artifact and standards output
|
|
233
241
|
|
|
@@ -20,7 +20,8 @@ The CLI emits stable JSON containing:
|
|
|
20
20
|
- `componentNaming`: `PascalCase | kebab-case | mixed | unknown` (decided from real file names under the component directory)
|
|
21
21
|
- `componentDir`, `serviceDir`, `hookDir` — first matching path found
|
|
22
22
|
- `samples[]` — up to 5 most-recently-modified files per kind
|
|
23
|
-
- `inconsistencies[]` —
|
|
23
|
+
- `hookConvention` — the hook directories' file CONTENTS read (not just paths): `{ directories[], inconsistencies[] }`. Each directory carries `namingPattern`, `dominantReturnShape` (null when no class is strictly dominant) + `dominantReturnSignature`, `offShapeHooks[]` (hooks whose shape CLASS differs — key drift is reported separately) plus the file-level `hookFileCount` and `mapperFiles[]`; each hook carries `{ file, name, usePrefix, returnShape, returnSignature }`. These are OBSERVED from the text, never type-checked: an import specifier matching /mapper/i is recorded against the FILE that contains it, and a file absent from `mapperFiles[]` is the absence of an observation, NOT evidence that its hooks map data inline
|
|
24
|
+
- `inconsistencies[]` — token names that have different values across sources, followed by the hook return-shape / hook-naming / mapper-delegation inconsistencies (all prefixed by the directory they belong to)
|
|
24
25
|
|
|
25
26
|
Copy these fields VERBATIM into `existing-system.md`. Do not re-classify tokens; do not invent additional samples.
|
|
26
27
|
|
|
@@ -59,6 +60,9 @@ Use the template below. Every value must come from the CLI JSON; leave a section
|
|
|
59
60
|
## Hooks convention
|
|
60
61
|
- Directory: <conventions.hookDir>
|
|
61
62
|
- Sample files: <conventions.samples filtered by kind=hook>
|
|
63
|
+
- Observed return shape: <per directory: namingPattern + dominantReturnShape + dominantReturnSignature from conventions.hookConvention.directories[*]; "- (none)" when the directory holds no exported function>
|
|
64
|
+
- Hooks deviating from it: <offShapeHooks; "- (none)" when empty>
|
|
65
|
+
- Mapper delegation: <mapperFiles.length of hookFileCount per directory — observed from import paths only, at FILE granularity; do not read a missing import as inlined mapping>
|
|
62
66
|
|
|
63
67
|
## Detected inconsistencies
|
|
64
68
|
<paste inconsistencies[*] verbatim; if empty, write "- (none)">
|
|
@@ -1,23 +1,65 @@
|
|
|
1
1
|
# Peaks-Loop Frontend-only development mode
|
|
2
2
|
|
|
3
3
|
> Extracted from `skills/peaks-code/SKILL.md` on 2026-06-09 (slice 019 — slim skill files to references) to keep SKILL.md under the 800-line cap from `common/coding-style.md`. The content below is the verbatim Frontend-only development mode section that was previously inline; nothing was paraphrased, just relocated.
|
|
4
|
+
>
|
|
5
|
+
> **2026-09-12 (`rd-routing-by-integration-mode`):** this file is now the routing home for all three integration modes, not just `prd-only`. `### Integration-mode routing (RD)` was added and the boolean-era text in `### Mode determination` was retargeted onto `.integrationMode`; the mock table and placeholder layout below belong to `prd-only`.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
`peaks scan archetype --json` classifies every project into exactly one of three integration modes (`.integrationMode`). Code and RD both branch on that value — the `frontendOnly` boolean is retained for back-compat only (the swarm plan still reads it) and is no longer a routing decision.
|
|
6
8
|
|
|
7
9
|
### Mode determination (deterministic — CLI is the source of truth)
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
Read `.integrationMode` and `.integrationModeReason` from the `peaks scan archetype --json` output and copy both into `.peaks/project-scan/project-scan.md` under `## Project mode`. Copy `frontendOnly` / `frontendOnlyReason` alongside them under `## Project mode`, labelled back-compat. Do NOT re-derive the decision from user phrasing — the three values are already deterministic:
|
|
12
|
+
|
|
13
|
+
| `.integrationMode` | `.integrationModeReason` | What is physically present |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `full-stack` | `backend-detected` | a backend framework, Next API routes, or a backend dir in this repo |
|
|
16
|
+
| `prd-plus-interface-doc` | `interface-doc-present` | an OpenAPI / proto document, and no backend here |
|
|
17
|
+
| `prd-only` | `no-backend-no-interface-doc` | neither |
|
|
18
|
+
|
|
19
|
+
A 0-1 bootstrap stub writes `unknown`; treat that as "not yet scanned", not as a fourth mode.
|
|
20
|
+
|
|
21
|
+
Back-compat mapping: `frontendOnly=false` covers `full-stack` **and** `prd-plus-interface-doc`; `frontendOnly=true` covers `prd-only`. That is why the boolean cannot route work — it merges two modes that need different procedures.
|
|
10
22
|
|
|
11
23
|
User-stated intent is **only** consulted when it conflicts with the CLI result. The two conflict cases:
|
|
12
24
|
|
|
13
|
-
- **CLI says `
|
|
14
|
-
- **CLI says `
|
|
25
|
+
- **CLI says `full-stack` but the user says "前端项目 / 没有后端 / 先 mock 数据"**: STOP and `AskUserQuestion` to confirm whether to override the scan (the repo probably contains a backend folder the user wants to ignore). Record the override decision and reason in `.peaks/project-scan/project-scan.md` under `## Project mode`.
|
|
26
|
+
- **CLI says `prd-only` but the user says "需要做后端 / 加 API"**: STOP and `AskUserQuestion` to confirm whether the request actually targets the missing backend (the user may be confused about repo scope, or there is a separate backend repo Code should switch to).
|
|
15
27
|
|
|
16
28
|
When there is no conflict, do not ask — the CLI value wins and the workflow proceeds.
|
|
17
29
|
|
|
30
|
+
### Integration-mode routing (RD) — three procedures
|
|
31
|
+
|
|
32
|
+
RD reads `.integrationMode` from `## Project mode` in `.peaks/project-scan/project-scan.md` and follows exactly one of the procedures below. Each mode has a different first command, a different contract source, and a different artifact; running one mode's steps under another mode is the collapse this routing exists to prevent.
|
|
33
|
+
|
|
34
|
+
#### `full-stack` — the backend is in this repo
|
|
35
|
+
|
|
36
|
+
1. **First:** read `## API` in `.peaks/project-scan/project-scan.md`. A real request wrapper, error shape, and endpoint inventory exist here — take them from the scan instead of re-deriving them by reading every service file.
|
|
37
|
+
2. If `.detected.swaggerPaths` from `peaks scan archetype --json` is non-empty, run `peaks scan api-diff <that path> --project <repo>` **before** editing any `*-api.types.ts`, and start from its `Exact` section.
|
|
38
|
+
3. **Boundary:** the DTO is the backend's own response type, imported at exactly one site per domain — the mapper path recorded under `## API → DTO ↔ ViewModel boundary` (`frontend-acl-mapper.md` rule 3: one mapper file per domain, reference shape `src/mappers/user.mapper.ts`). The internal ViewModel file it targets is the one recorded in that same `## API` row (reference shape `src/models/<domain>.ts`).
|
|
39
|
+
4. **Produces:** that mapper file, and the `## Red-line scope` section of `.peaks/_runtime/<sessionId>/rd/requests/<rid>.md` naming the in-scope endpoints. Close with `peaks scan diff-vs-scope --rid <rid> --project <repo>` (Gate B8).
|
|
40
|
+
5. **MUST NOT** mock. There is no missing contract to guess, so no `mock-plan.md` entry and no `mock/*.ts` file is written. This is not honour-system: an inline mock literal in a changed file under `src/` fails `rl-mock-placement-001` (`src/services/audit/enforcers/mock-placement.ts`).
|
|
41
|
+
|
|
42
|
+
#### `prd-plus-interface-doc` — a document exists, no backend here
|
|
43
|
+
|
|
44
|
+
1. **First:** `peaks scan api-diff <doc> --project <repo>`, where `<doc>` is a path from `.detected.swaggerPaths`. Run it **before any type is written** — the whole point of this mode is that the recorded interfaces start from the document rather than from a guess. The `Exact` section is the change list; it is exact because both sides are parsed.
|
|
45
|
+
2. Write or refresh `src/services/types/<feature>-api.types.ts` — one interface per endpoint the `Exact` section names. Every field must trace to a line of the document.
|
|
46
|
+
3. **Boundary:** the mapper imports its DTO from the doc-derived `*-api.types.ts` and its ViewModel from the internal model file recorded under `## API → DTO ↔ ViewModel boundary`. The mapper is what absorbs a doc revision: after the document changes, re-run step 1 and the mapper file appears in the `Candidate mentions` section as the place to edit.
|
|
47
|
+
4. **Produces:** the doc-derived `*-api.types.ts`, the mapper, and the `api-diff` `Exact` output pasted into `.peaks/_runtime/<sessionId>/rd/requests/<rid>.md`.
|
|
48
|
+
5. **MUST NOT** author a DTO from memory — that is `prd-only`'s guess, and here it would silently shadow the document. **MUST NOT** treat the `Candidate mentions` section as exact: it is a name-grep over the consumer's source, so it over-reports test fixtures and under-reports i18n keys, AntD `columns` arrays, `data-field` attributes, and monorepo barrels.
|
|
49
|
+
6. **Not checkable in this repo:** nothing detects doc↔server drift. `api-diff` compares the document against what the project recorded; if the document itself is stale, the interfaces compile clean and the UI renders `undefined`. `api-diff` prints this limitation in its own output footer — do not restate it as covered.
|
|
50
|
+
|
|
51
|
+
#### `prd-only` — no backend, no document
|
|
52
|
+
|
|
53
|
+
1. **First:** pick a row of the mock-strategy table below from the project's data-fetching pattern in `## API`, then write `.peaks/_runtime/<sessionId>/rd/mock-plan.md` **before producing any mock file** — chosen strategy, planned file paths, one-line rationale per file (`mock-plan.md` is the source of truth for mock locations across runs; RD reads it before writing code and QA reads it before writing test cases). If the table's last row applies ("cannot decide from scan alone"), STOP and `AskUserQuestion`.
|
|
54
|
+
2. Define the response interfaces first: `src/services/types/<feature>-api.types.ts`, per the placeholder layout below. These interfaces are an **authored guess** — mark them as such in the file header.
|
|
55
|
+
3. **Boundary:** write the mapper against the *interface*, never against the mock module. That is what keeps the guess cheap: when the real document lands, the mock file is deleted and the mapper's import target changes — no component, hook, or store is touched.
|
|
56
|
+
4. **Produces:** `mock-plan.md`, the `*-api.types.ts` interfaces, `mock/<feature>-mock.ts`, and the mapper.
|
|
57
|
+
5. **MUST NOT** write mock data inline in a component file — mock files live in the mock directory, and an inline mock literal in a changed file under `src/` fails `rl-mock-placement-001`. Every mock file carries the `// MOCK: Replace with real API call when swagger.json is available` header (mock rule 4 below).
|
|
58
|
+
6. **Exit path:** when a document appears, run `peaks scan api-diff <doc> --project <repo>` and follow §"Mock-to-real migration path" below. **Not checkable in this repo:** nothing detects "a real document now exists for this domain" automatically, and the conditional contract artifact (`.peaks/api-contract.json` with staleness detection, design spec §2.6 / S3) is **not built**. Until it is, step 1 of the migration is a run RD schedules manually.
|
|
59
|
+
|
|
18
60
|
### Mock data strategy selection
|
|
19
61
|
|
|
20
|
-
Code records the chosen
|
|
62
|
+
This table belongs to `prd-only`. Under `full-stack` and `prd-plus-interface-doc` a real contract source exists (the backend type, or the document), so no mock is written. Under `prd-only`, Code records the chosen strategy in `.peaks/_runtime/<sessionId>/rd/mock-plan.md` under a `## Mock Data Strategy` section — the same file mock rule 5 below requires before any mock file is produced. The choice depends on the project scan results:
|
|
21
63
|
|
|
22
64
|
| Project data-fetching pattern | Recommended mock approach | Rationale |
|
|
23
65
|
|---|---|---|
|
|
@@ -81,7 +123,7 @@ Never silently fall back to unauthenticated `fetch` or `WebFetch` for authentica
|
|
|
81
123
|
1. Read `.peaks/_runtime/<sessionId>/prd/requests/<rid>.md` body.
|
|
82
124
|
2. Lowercase + strip markdown; check regex `\b(页面|组件|表单|弹窗|表格|样式|布局|交互|UI|UX|page|component|form|modal|table|styling|layout|interaction|frontend|前端)\b`.
|
|
83
125
|
3. If match count ≥ 1 → `frontendKeywordHit=true`.
|
|
84
|
-
4. If `frontendOnly` (from project-scan) is `true` and no keyword hit → UI joins anyway (frontend-only project, even non-visual changes may need visual sanity for regressions).
|
|
126
|
+
4. If `frontendOnly` (back-compat boolean from `## Project mode` in project-scan — the UI-inclusion signal only; it is not the integration-mode router) is `true` and no keyword hit → UI joins anyway (frontend-only project, even non-visual changes may need visual sanity for regressions).
|
|
85
127
|
5. If `frontendOnly` is `false` and no keyword hit → UI skipped.
|
|
86
128
|
|
|
87
129
|
Code records the pre-flight result in `sc/swarm-plan.json` so the audit trail shows why UI was or was not included.
|
|
@@ -18,8 +18,10 @@ The command emits a stable JSON envelope with these fields you copy verbatim int
|
|
|
18
18
|
|
|
19
19
|
- `archetype`: `greenfield | legacy-frontend | legacy-fullstack | frontend-monorepo | unknown`
|
|
20
20
|
- `confidence`: `high | medium | low`
|
|
21
|
-
- `frontendOnly`: `true | false`
|
|
21
|
+
- `frontendOnly`: `true | false` (back-compat boolean — keep it recorded)
|
|
22
22
|
- `frontendOnlyReason`: short string explaining the decision
|
|
23
|
+
- `integrationMode`: `full-stack | prd-plus-interface-doc | prd-only` — which of the three frontend integration scenarios the project is in (`full-stack` = backend in this repo, contract is a shared interface; `prd-plus-interface-doc` = no backend here but an interface doc exists, derive the ACL from it; `prd-only` = no backend and no doc yet, mock and keep the boundary cheap to change)
|
|
24
|
+
- `integrationModeReason`: short string explaining the decision (`backend-detected | interface-doc-present | no-backend-no-interface-doc`)
|
|
23
25
|
- `signals[]`: each signal's name, matched flag, and detail (paste under `## Archetype → Signals matched`)
|
|
24
26
|
- `detected`: raw filesystem facts (package.json presence, backend frameworks, swagger paths, monorepo configs, src file count, lockfile age)
|
|
25
27
|
|
|
@@ -106,9 +108,16 @@ Grep `src/` for outdated patterns and list them as constraints in `project-scan.
|
|
|
106
108
|
- Signals matched: <bullet list of signals that drove the decision>
|
|
107
109
|
|
|
108
110
|
## Project mode
|
|
111
|
+
- Integration mode: <full-stack | prd-plus-interface-doc | prd-only> (from `peaks scan archetype --json` → `.integrationMode`)
|
|
112
|
+
- Integration mode reason: <backend-detected | interface-doc-present | no-backend-no-interface-doc>
|
|
109
113
|
- Frontend-only: <true | false>
|
|
110
114
|
- Reason: <archetype-derived | user-stated | backend-detected>
|
|
111
115
|
|
|
116
|
+
The integration mode is one of exactly three values, all derived by `peaks scan archetype`
|
|
117
|
+
from signals it already detects — never judged by hand. The 0-1 bootstrap stub writes
|
|
118
|
+
`unknown` instead, because on that path no archetype report has been produced yet; treat
|
|
119
|
+
`unknown` as "not yet scanned", not as a fourth mode.
|
|
120
|
+
|
|
112
121
|
## Build tool
|
|
113
122
|
- Framework: <name> <version>
|
|
114
123
|
- Config file: <path>
|
|
@@ -128,6 +137,16 @@ Grep `src/` for outdated patterns and list them as constraints in `project-scan.
|
|
|
128
137
|
- Routing: <name>
|
|
129
138
|
- Data fetching: <name>
|
|
130
139
|
|
|
140
|
+
## API
|
|
141
|
+
- Record file paths, not summaries — a later slice diffs against them, and a path that has moved is a finding.
|
|
142
|
+
- Base URL / env configuration: <env-var name(s) and the file that reads them, e.g. `VITE_API_BASE` in `src/config/env.ts` | none>
|
|
143
|
+
- Request wrapper + interceptors: <path of the single HTTP entry point, e.g. `src/services/http/client.ts`, and the mechanism used for cross-cutting concerns — axios interceptors, a `fetch` wrapper, ofetch hooks — with the auth/retry/header logic named | none>
|
|
144
|
+
- Error-handling shape: <what the wrapper hands callers on failure — thrown `Error` subclass, `{ ok, data, error }` result, HTTP-status branch — and the file where it is normalised>
|
|
145
|
+
- Hook convention: <naming pattern and directory, e.g. `use<Domain>` under `src/hooks/`, the return shape (`{ data, loading, error }` | `[data, actions]` | query object), and whether the hook does its own mapping or delegates>
|
|
146
|
+
- Endpoint inventory: <one bullet per endpoint the frontend actually calls — `METHOD /path` → calling file; when an interface doc exists, cross-check with `peaks scan api-diff <path-to-doc>`>
|
|
147
|
+
- Existing mock strategy: <mechanism in use today (module mock | MSW | static fixture module | inline) and where mock files live. Per `frontend-only-mode.md` mocks MUST NOT be inline in component files — an inline mock here is a finding, record it>
|
|
148
|
+
- DTO ↔ ViewModel boundary: <path of the per-domain mapper file, e.g. `src/mappers/user.mapper.ts`, and the internal ViewModel file it targets, per `skills/bee/peaks-rd/references/frontend-acl-mapper.md`. Record `none` explicitly when there is no mapper — that is a finding, not a blank>
|
|
149
|
+
|
|
131
150
|
## Library versions
|
|
132
151
|
- Source: output of `peaks scan libraries --project <repo> --json` (see Gate A; cross-check diff imports against `schemas/library-breaking-changes.data.json` in `peaks-rd` preflight)
|
|
133
152
|
- Total: <count from scan.libraries.totalCount>
|
|
@@ -21,6 +21,7 @@ Slice L3.2 ships 69 doctor checks. The most user-relevant ones:
|
|
|
21
21
|
## Integration (third-party hooks)
|
|
22
22
|
|
|
23
23
|
- **`integration:gateguard-peaks-conflict`** — warns when `gateguard-fact-force` is installed without a `.peaks/**` skip pattern (the 3rd-party hook would block all peaks-qa .peaks/ artifact writes)
|
|
24
|
+
- **`integration:ecc-hooks-schema-drift`** — warns (cosmetic, never fails the run) when the 3rd-party ECC plugin's `hooks/hooks.json` carries keys Claude Code ignores (`$schema` at the root; `description` + `id` per matcher group), which is what prints `ecc: hooks.json: unknown keys ... ignored` at startup
|
|
24
25
|
|
|
25
26
|
## Skills
|
|
26
27
|
|