peaks-loop 4.0.11 → 4.0.13
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 +47 -0
- package/dist/cli/commands/code-commands.js +2 -0
- package/dist/cli/commands/code-orchestrator-can-do.d.ts +17 -0
- package/dist/cli/commands/code-orchestrator-can-do.js +75 -0
- package/dist/cli/commands/core/doctor-command.js +26 -3
- package/dist/cli/commands/workspace/reconcile-command.js +5 -2
- package/dist/services/code/orchestrator-can-do.d.ts +114 -0
- package/dist/services/code/orchestrator-can-do.js +205 -0
- package/dist/services/doctor/doctor-service/checks/multi-binary-drift.d.ts +61 -0
- package/dist/services/doctor/doctor-service/checks/multi-binary-drift.js +268 -0
- package/dist/services/doctor/doctor-service/checks/skill-presence.js +1 -1
- package/dist/services/doctor/doctor-service/checks/workspace-layout.d.ts +5 -2
- package/dist/services/doctor/doctor-service/checks/workspace-layout.js +70 -9
- package/dist/services/doctor/doctor-service/plugin-registry.js +2 -0
- package/dist/services/doctor/doctor-service/report/final-summary.d.ts +13 -5
- package/dist/services/doctor/doctor-service/report/final-summary.js +19 -9
- package/dist/services/doctor/doctor-service/types.d.ts +64 -0
- package/dist/services/doctor/doctor-service.d.ts +2 -1
- package/dist/services/doctor/doctor-service.js +1 -0
- package/dist/services/doctor/index.d.ts +1 -1
- package/dist/services/doctor/index.js +1 -1
- package/dist/services/hooks/presence-marker-detector.d.ts +7 -5
- package/dist/services/hooks/presence-marker-detector.js +45 -85
- package/dist/services/migration/v2-10-to-v2-11-service.js +1 -3
- package/dist/services/sc/sc-service.d.ts +11 -7
- package/dist/services/sc/sc-service.js +29 -26
- package/dist/services/session/binding-store.d.ts +6 -6
- package/dist/services/skills/hooks-settings-service.d.ts +1 -1
- package/dist/services/skills/hooks-settings-service.js +1 -1
- package/dist/services/skills/skill-presence-service.d.ts +0 -1
- package/dist/services/skills/skill-presence-service.js +112 -102
- package/dist/services/skills/skill-statusline-renderer.d.ts +3 -1
- package/dist/services/skills/skill-statusline-renderer.js +37 -1
- package/dist/services/skills/skill-statusline-service.d.ts +14 -0
- package/dist/services/skills/skill-statusline-service.js +109 -53
- package/dist/services/skills/skill-statusline-sid-suffix.d.ts +55 -0
- package/dist/services/skills/skill-statusline-sid-suffix.js +65 -0
- package/package.json +4 -4
- package/skills/peaks-code/SKILL.md +6 -0
- package/skills/peaks-code/references/session-overload-signal-index.md +60 -0
|
@@ -1,28 +1,34 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
1
|
import { resolve } from 'node:path';
|
|
3
2
|
import { findProjectRoot } from '../config/config-safety.js';
|
|
4
3
|
import { decideCompactStatusline } from '../compact-statusline/compact-statusline-service.js';
|
|
5
4
|
import { getSessionIdCanonical } from '../session/session-manager.js';
|
|
6
5
|
import { resolveActiveSkillForCaller } from '../audit/enforcers/active-skill-resolver.js';
|
|
6
|
+
import { listPresenceLeases } from './presence-lease-service.js';
|
|
7
7
|
import { readActiveDispatchIndex } from '../dispatch/dispatch-record-writer.js';
|
|
8
8
|
/**
|
|
9
9
|
* Out-of-band Peaks skill status renderer for the Claude Code statusLine.
|
|
10
10
|
*
|
|
11
11
|
* Claude Code invokes the configured statusLine command on every turn and pipes
|
|
12
|
-
* a JSON session payload on stdin. This renderer reads the
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* by the
|
|
18
|
-
*
|
|
12
|
+
* a JSON session payload on stdin. This renderer reads the canonical
|
|
13
|
+
* sid-scoped lease projection
|
|
14
|
+
* (`.peaks/_runtime/<sid>/leases/presence-<caller>-<workflow>.json` +
|
|
15
|
+
* the per-caller index under `presence-index/<caller>.json`) and prints a
|
|
16
|
+
* single line that Claude Code paints at the bottom of the terminal. Because
|
|
17
|
+
* it is rendered by the harness — not emitted as LLM tokens — the signal
|
|
18
|
+
* cannot be forgotten by the model, cannot be confused with normal output,
|
|
19
|
+
* and survives context compaction.
|
|
19
20
|
*
|
|
20
21
|
* This module is intentionally READ-ONLY. Unlike getSkillPresence in
|
|
21
22
|
* skill-presence-service.ts, it never deletes or rewrites the presence file:
|
|
22
23
|
* the statusLine runs on every turn and must have zero side effects.
|
|
24
|
+
*
|
|
25
|
+
* Slice 2026-08-05-statusline-sid-scoped-lease-B: the read no longer falls
|
|
26
|
+
* back to the project-level `.peaks/_runtime/active-skill.json` (or its
|
|
27
|
+
* legacy `.peaks/.active-skill.json`). The canonical lease projection is
|
|
28
|
+
* the only source. When `callerId === null` (non-IDE caller), the read
|
|
29
|
+
* picks the most recent in-flight lease across all callers; this is the
|
|
30
|
+
* documented back-compat path.
|
|
23
31
|
*/
|
|
24
|
-
const PRESENCE_FILE = '.peaks/_runtime/active-skill.json';
|
|
25
|
-
const PRESENCE_FILE_LEGACY = '.peaks/.active-skill.json';
|
|
26
32
|
const STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
|
|
27
33
|
function resolveCwdFromStdin(stdin) {
|
|
28
34
|
const fromWorkspace = stdin?.workspace?.current_dir ?? stdin?.workspace?.project_dir;
|
|
@@ -113,61 +119,99 @@ function readActiveLeaf(projectRoot, sessionId) {
|
|
|
113
119
|
* Read the presence file without any side effects. Returns null when the file is
|
|
114
120
|
* absent (idle) and a sentinel object for malformed content (invalid-presence).
|
|
115
121
|
*
|
|
116
|
-
*
|
|
117
|
-
* (
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
122
|
+
* Both branches now route through the canonical sid-scoped lease projection
|
|
123
|
+
* (slice 2026-08-05-statusline-sid-scoped-lease-B):
|
|
124
|
+
* - `callerId !== null` → `resolveActiveSkillForCaller` with the canonical
|
|
125
|
+
* (non-legacy) lease projection, filtered to this callerId. When the
|
|
126
|
+
* callerId-filtered resolution returns `source: 'none'` (no lease under
|
|
127
|
+
* this callerId), retry once with `callerId: null` so the read falls back
|
|
128
|
+
* to the session's most-recent in-flight lease. AC4 multi-tenant isolation
|
|
129
|
+
* is preserved: when callerId A DOES have a lease, the first call returns
|
|
130
|
+
* it and the fallback never fires — callerId B's lease is never surfaced
|
|
131
|
+
* to callerId A. (Slice 2026-08-05-statusline-empty-render-and-short-sid-suffix.)
|
|
132
|
+
* - `callerId === null` → enumerate `listPresenceLeases` for the
|
|
133
|
+
* canonical session and pick the most recent in-flight lease. Back-compat
|
|
134
|
+
* for non-IDE callers (e.g. legacy CLI invocations) that have no callerId.
|
|
135
|
+
*
|
|
136
|
+
* No fallback to `.peaks/_runtime/active-skill.json` (or its legacy path):
|
|
137
|
+
* the canonical lease projection is the single source of truth. When no
|
|
138
|
+
* in-flight leases exist, the read returns `{ presence: null, invalid: false }`
|
|
139
|
+
* and the renderer falls back to the idle state.
|
|
121
140
|
*/
|
|
122
141
|
function readPresenceReadOnly(projectRoot, callerId) {
|
|
123
142
|
if (callerId !== null) {
|
|
143
|
+
let firstResolution = null;
|
|
124
144
|
try {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
145
|
+
firstResolution = resolveActiveSkillForCaller(projectRoot, { callerId });
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return { presence: null, invalid: true };
|
|
149
|
+
}
|
|
150
|
+
if (firstResolution.source !== 'none' && firstResolution.skill !== null) {
|
|
129
151
|
return {
|
|
130
152
|
presence: {
|
|
131
|
-
skill:
|
|
132
|
-
...(
|
|
153
|
+
skill: firstResolution.skill,
|
|
154
|
+
...(firstResolution.mode !== null ? { mode: firstResolution.mode } : {}),
|
|
133
155
|
},
|
|
134
156
|
invalid: false,
|
|
135
157
|
};
|
|
136
158
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
159
|
+
// callerId didn't match any lease — fall back to the session's most
|
|
160
|
+
// recent in-flight lease (the callerId === null branch below). This
|
|
161
|
+
// rescues the case where the harness pipes a `CLAUDE_CODE_SESSION_ID`
|
|
162
|
+
// that differs from the active lease's callerId (peaks-code sessions
|
|
163
|
+
// started from a different outer session id, etc.).
|
|
164
|
+
return readPresenceReadOnly(projectRoot, null);
|
|
165
|
+
}
|
|
166
|
+
// callerId === null branch: enumerate the canonical session dir's leases
|
|
167
|
+
// and pick the most recent in-flight lease. This is the back-compat path
|
|
168
|
+
// for non-IDE callers that don't supply a callerId.
|
|
169
|
+
let sessionId = null;
|
|
170
|
+
try {
|
|
171
|
+
sessionId = getSessionIdCanonical(projectRoot);
|
|
140
172
|
}
|
|
141
|
-
|
|
142
|
-
// Back-compat: prefer the new canonical path; fall back to the legacy
|
|
143
|
-
// `.peaks/.active-skill.json` for one minor release.
|
|
144
|
-
const pathToRead = existsSync(presencePath) ? presencePath : resolve(projectRoot, PRESENCE_FILE_LEGACY);
|
|
145
|
-
if (!existsSync(pathToRead)) {
|
|
173
|
+
catch {
|
|
146
174
|
return { presence: null, invalid: false };
|
|
147
175
|
}
|
|
176
|
+
if (sessionId === null) {
|
|
177
|
+
return { presence: null, invalid: false };
|
|
178
|
+
}
|
|
179
|
+
let leases = [];
|
|
148
180
|
try {
|
|
149
|
-
|
|
150
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
151
|
-
return { presence: null, invalid: true };
|
|
152
|
-
}
|
|
153
|
-
const candidate = parsed;
|
|
154
|
-
if (typeof candidate.skill !== 'string' || candidate.skill.length === 0) {
|
|
155
|
-
return { presence: null, invalid: true };
|
|
156
|
-
}
|
|
157
|
-
return {
|
|
158
|
-
presence: {
|
|
159
|
-
skill: candidate.skill,
|
|
160
|
-
...(typeof candidate.mode === 'string' ? { mode: candidate.mode } : {}),
|
|
161
|
-
...(typeof candidate.gate === 'string' ? { gate: candidate.gate } : {}),
|
|
162
|
-
...(typeof candidate.setAt === 'string' ? { setAt: candidate.setAt } : {}),
|
|
163
|
-
...(typeof candidate.claudeSessionId === 'string' ? { claudeSessionId: candidate.claudeSessionId } : {})
|
|
164
|
-
},
|
|
165
|
-
invalid: false
|
|
166
|
-
};
|
|
181
|
+
leases = listPresenceLeases(projectRoot, sessionId);
|
|
167
182
|
}
|
|
168
183
|
catch {
|
|
169
184
|
return { presence: null, invalid: true };
|
|
170
185
|
}
|
|
186
|
+
const inFlight = leases
|
|
187
|
+
.filter((l) => l.status === 'preparing' || l.status === 'running')
|
|
188
|
+
.filter((l) => typeof l.skill === 'string' && l.skill.length > 0);
|
|
189
|
+
if (inFlight.length === 0) {
|
|
190
|
+
return { presence: null, invalid: false };
|
|
191
|
+
}
|
|
192
|
+
// Most recent wins — sort by `lastHeartbeat` desc, fall back to `startedAt`.
|
|
193
|
+
const sorted = inFlight.slice().sort((a, b) => {
|
|
194
|
+
const hb = b.lastHeartbeat.localeCompare(a.lastHeartbeat);
|
|
195
|
+
if (hb !== 0)
|
|
196
|
+
return hb;
|
|
197
|
+
return b.startedAt.localeCompare(a.startedAt);
|
|
198
|
+
});
|
|
199
|
+
const latest = sorted[0];
|
|
200
|
+
if (latest === undefined || typeof latest.skill !== 'string' || latest.skill.length === 0) {
|
|
201
|
+
return { presence: null, invalid: false };
|
|
202
|
+
}
|
|
203
|
+
// Leases on disk may carry an optional `mode` field (the lease constructor
|
|
204
|
+
// spreads `input.mode` when present); the typed `SkillPresenceLease` does
|
|
205
|
+
// not declare it, so widen the read shape here.
|
|
206
|
+
const latestMode = latest.mode;
|
|
207
|
+
return {
|
|
208
|
+
presence: {
|
|
209
|
+
skill: latest.skill,
|
|
210
|
+
...(typeof latestMode === 'string' && latestMode.length > 0 ? { mode: latestMode } : {}),
|
|
211
|
+
setAt: latest.startedAt,
|
|
212
|
+
},
|
|
213
|
+
invalid: false,
|
|
214
|
+
};
|
|
171
215
|
}
|
|
172
216
|
export function buildStatusLineModel(stdin, nowMs) {
|
|
173
217
|
const cwd = resolveCwdFromStdin(stdin);
|
|
@@ -176,17 +220,30 @@ export function buildStatusLineModel(stdin, nowMs) {
|
|
|
176
220
|
// and reads `.peaks/_runtime/<sid>/compact-lifecycle.json` (or its legacy
|
|
177
221
|
// fallbacks). It replaces the active skill content when kind != 'none'.
|
|
178
222
|
const compact = readCompactState(projectRoot, nowMs);
|
|
223
|
+
// Slice 2026-08-05-statusline-sid-only-marker: resolve the canonical
|
|
224
|
+
// session id once, up front, so the renderer can read it from the model
|
|
225
|
+
// without re-running `getSessionIdCanonical`. `null` when the project
|
|
226
|
+
// root is unbound OR when no `.peaks/_runtime/<sid>/` session exists.
|
|
227
|
+
let sessionId = null;
|
|
228
|
+
if (projectRoot !== null) {
|
|
229
|
+
try {
|
|
230
|
+
sessionId = getSessionIdCanonical(projectRoot);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
sessionId = null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
179
236
|
if (projectRoot === null) {
|
|
180
|
-
return { state: 'idle', projectRoot: null, presence: null, ageMs: null, compact, activeLeaf: null };
|
|
237
|
+
return { state: 'idle', projectRoot: null, presence: null, ageMs: null, compact, activeLeaf: null, sessionId: null };
|
|
181
238
|
}
|
|
182
239
|
// callerId resolves the read-side isolation; back-compat is `null`.
|
|
183
240
|
const callerId = resolveCallerId(stdin);
|
|
184
241
|
const { presence, invalid } = readPresenceReadOnly(projectRoot, callerId);
|
|
185
242
|
if (invalid) {
|
|
186
|
-
return { state: 'invalid-presence', projectRoot, presence: null, ageMs: null, compact, activeLeaf: null };
|
|
243
|
+
return { state: 'invalid-presence', projectRoot, presence: null, ageMs: null, compact, activeLeaf: null, sessionId };
|
|
187
244
|
}
|
|
188
245
|
if (presence === null) {
|
|
189
|
-
return { state: 'idle', projectRoot, presence: null, ageMs: null, compact, activeLeaf: null };
|
|
246
|
+
return { state: 'idle', projectRoot, presence: null, ageMs: null, compact, activeLeaf: null, sessionId };
|
|
190
247
|
}
|
|
191
248
|
// Session binding: when the presence was stamped with a Claude session id and
|
|
192
249
|
// the live session (from stdin) is a different one, the recorded skill belongs
|
|
@@ -195,7 +252,7 @@ export function buildStatusLineModel(stdin, nowMs) {
|
|
|
195
252
|
// fall back to the time-based behavior below for backward compatibility.
|
|
196
253
|
const liveSessionId = typeof stdin?.session_id === 'string' && stdin.session_id.length > 0 ? stdin.session_id : null;
|
|
197
254
|
if (presence.claudeSessionId && liveSessionId && presence.claudeSessionId !== liveSessionId) {
|
|
198
|
-
return { state: 'idle', projectRoot, presence: null, ageMs: null, compact, activeLeaf: null };
|
|
255
|
+
return { state: 'idle', projectRoot, presence: null, ageMs: null, compact, activeLeaf: null, sessionId };
|
|
199
256
|
}
|
|
200
257
|
const setAtMs = presence.setAt ? Date.parse(presence.setAt) : Number.NaN;
|
|
201
258
|
const ageMs = Number.isNaN(setAtMs) ? null : nowMs - setAtMs;
|
|
@@ -206,13 +263,12 @@ export function buildStatusLineModel(stdin, nowMs) {
|
|
|
206
263
|
// bee skill (e.g. `peaks-rd`) alongside the orchestrator (e.g. `peaks-code`).
|
|
207
264
|
let activeLeaf = null;
|
|
208
265
|
try {
|
|
209
|
-
const sessionId = getSessionIdCanonical(projectRoot);
|
|
210
266
|
activeLeaf = readActiveLeaf(projectRoot, sessionId);
|
|
211
267
|
}
|
|
212
268
|
catch {
|
|
213
269
|
activeLeaf = null;
|
|
214
270
|
}
|
|
215
|
-
return { state, projectRoot, presence, ageMs, compact, activeLeaf };
|
|
271
|
+
return { state, projectRoot, presence, ageMs, compact, activeLeaf, sessionId };
|
|
216
272
|
}
|
|
217
273
|
/**
|
|
218
274
|
* Read-only compact state resolver. Resolves the canonical session id for the
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure short-sid suffix helpers for the Peaks statusLine.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `skill-statusline-renderer.ts` (slice
|
|
5
|
+
* 2026-08-05-statusline-sid-only-marker-and-multi-binary-drift-guard
|
|
6
|
+
* repair cycle) to keep the renderer under the Karpathy 800-line cap.
|
|
7
|
+
* The module is I/O-free and only depends on the read-only
|
|
8
|
+
* `StatusLineModel` shape from `skill-statusline-service.ts` plus the
|
|
9
|
+
* `StatusPalette` interface still owned by the renderer. Re-exports
|
|
10
|
+
* both helpers so the existing test surface (`formatShortSid` direct
|
|
11
|
+
* tests + `computeRootSuffix` helper-level tests in
|
|
12
|
+
* `tests/unit/skills/skill-statusline-sid-only-marker.test.ts`) keeps
|
|
13
|
+
* importing from the renderer for back-compat.
|
|
14
|
+
*
|
|
15
|
+
* Pure module — no I/O, no clock dependency, no side effects.
|
|
16
|
+
*/
|
|
17
|
+
import type { StatusLineModel } from './skill-statusline-service.js';
|
|
18
|
+
/**
|
|
19
|
+
* Minimal palette slice consumed by {@link computeRootSuffix}. The full
|
|
20
|
+
* `StatusPalette` is defined as a module-private interface inside the
|
|
21
|
+
* renderer; the suffix helper only needs `trailSeparator`. Re-declared
|
|
22
|
+
* structurally so the helper has no upward import on the renderer.
|
|
23
|
+
*/
|
|
24
|
+
export interface SidSuffixPalette {
|
|
25
|
+
readonly trailSeparator: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Format a session id as a short, terminal-friendly tag.
|
|
29
|
+
*
|
|
30
|
+
* Returns the last kebab-segment of `sessionId` (e.g.
|
|
31
|
+
* `2026-08-04-session-3fe1be` → `3fe1be`), or the empty string when
|
|
32
|
+
* the input is empty. Pure; safe for ASCII rendering across
|
|
33
|
+
* PowerShell, Git Bash, and zsh (AC7).
|
|
34
|
+
*
|
|
35
|
+
* Slice 2026-08-05-statusline-empty-render-and-short-sid-suffix.
|
|
36
|
+
*/
|
|
37
|
+
export declare function formatShortSid(sessionId: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Compute the project-root cell of the status line. Returns the
|
|
40
|
+
* `<trailSep><root>` portion (and optionally the ` [shortSid]` suffix).
|
|
41
|
+
*
|
|
42
|
+
* Pure helper — no I/O. The `sessionId` is read from the model that the
|
|
43
|
+
* service already resolved once via `getSessionIdCanonical`. Per-state
|
|
44
|
+
* matrix (see the docstring inside `renderStatusLine` in
|
|
45
|
+
* `skill-statusline-renderer.ts` for the AC/PB references):
|
|
46
|
+
*
|
|
47
|
+
* - state === 'invalid-presence' → no sid (G2 invariant)
|
|
48
|
+
* - state === 'idle' | 'stale' | 'active' → sid iff model.sessionId resolves
|
|
49
|
+
* to a non-empty shortSid string
|
|
50
|
+
* - rootLabelText === '' → empty string
|
|
51
|
+
*
|
|
52
|
+
* Exported so tests can drive the helper without spinning up the full
|
|
53
|
+
* renderer (marquee, palette, etc.).
|
|
54
|
+
*/
|
|
55
|
+
export declare function computeRootSuffix(model: StatusLineModel, rootLabelText: string, palette: SidSuffixPalette): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure short-sid suffix helpers for the Peaks statusLine.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `skill-statusline-renderer.ts` (slice
|
|
5
|
+
* 2026-08-05-statusline-sid-only-marker-and-multi-binary-drift-guard
|
|
6
|
+
* repair cycle) to keep the renderer under the Karpathy 800-line cap.
|
|
7
|
+
* The module is I/O-free and only depends on the read-only
|
|
8
|
+
* `StatusLineModel` shape from `skill-statusline-service.ts` plus the
|
|
9
|
+
* `StatusPalette` interface still owned by the renderer. Re-exports
|
|
10
|
+
* both helpers so the existing test surface (`formatShortSid` direct
|
|
11
|
+
* tests + `computeRootSuffix` helper-level tests in
|
|
12
|
+
* `tests/unit/skills/skill-statusline-sid-only-marker.test.ts`) keeps
|
|
13
|
+
* importing from the renderer for back-compat.
|
|
14
|
+
*
|
|
15
|
+
* Pure module — no I/O, no clock dependency, no side effects.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Format a session id as a short, terminal-friendly tag.
|
|
19
|
+
*
|
|
20
|
+
* Returns the last kebab-segment of `sessionId` (e.g.
|
|
21
|
+
* `2026-08-04-session-3fe1be` → `3fe1be`), or the empty string when
|
|
22
|
+
* the input is empty. Pure; safe for ASCII rendering across
|
|
23
|
+
* PowerShell, Git Bash, and zsh (AC7).
|
|
24
|
+
*
|
|
25
|
+
* Slice 2026-08-05-statusline-empty-render-and-short-sid-suffix.
|
|
26
|
+
*/
|
|
27
|
+
export function formatShortSid(sessionId) {
|
|
28
|
+
if (sessionId.length === 0)
|
|
29
|
+
return '';
|
|
30
|
+
const last = sessionId.split('-').pop();
|
|
31
|
+
return last ?? sessionId;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Compute the project-root cell of the status line. Returns the
|
|
35
|
+
* `<trailSep><root>` portion (and optionally the ` [shortSid]` suffix).
|
|
36
|
+
*
|
|
37
|
+
* Pure helper — no I/O. The `sessionId` is read from the model that the
|
|
38
|
+
* service already resolved once via `getSessionIdCanonical`. Per-state
|
|
39
|
+
* matrix (see the docstring inside `renderStatusLine` in
|
|
40
|
+
* `skill-statusline-renderer.ts` for the AC/PB references):
|
|
41
|
+
*
|
|
42
|
+
* - state === 'invalid-presence' → no sid (G2 invariant)
|
|
43
|
+
* - state === 'idle' | 'stale' | 'active' → sid iff model.sessionId resolves
|
|
44
|
+
* to a non-empty shortSid string
|
|
45
|
+
* - rootLabelText === '' → empty string
|
|
46
|
+
*
|
|
47
|
+
* Exported so tests can drive the helper without spinning up the full
|
|
48
|
+
* renderer (marquee, palette, etc.).
|
|
49
|
+
*/
|
|
50
|
+
export function computeRootSuffix(model, rootLabelText, palette) {
|
|
51
|
+
if (!rootLabelText)
|
|
52
|
+
return '';
|
|
53
|
+
let suffix = `${palette.trailSeparator}${rootLabelText}`;
|
|
54
|
+
if (model.state === 'invalid-presence')
|
|
55
|
+
return suffix;
|
|
56
|
+
// Defensive: older callers may build a model without `sessionId`
|
|
57
|
+
// (the field is optional in some test fixtures predating the
|
|
58
|
+
// sid-only-marker slice). Treat undefined the same as null.
|
|
59
|
+
if (model.sessionId === null || model.sessionId === undefined)
|
|
60
|
+
return suffix;
|
|
61
|
+
const short = formatShortSid(model.sessionId);
|
|
62
|
+
if (short.length === 0)
|
|
63
|
+
return suffix;
|
|
64
|
+
return `${suffix} [${short}]`;
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-loop",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.13",
|
|
4
4
|
"description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
|
|
5
5
|
"author": "SquabbyZ",
|
|
6
6
|
"keywords": [
|
|
@@ -100,9 +100,9 @@
|
|
|
100
100
|
"headroom-ai": "0.22.4",
|
|
101
101
|
"yaml": "^2.9.0",
|
|
102
102
|
"zod": "^3.25.76",
|
|
103
|
-
"peaks-loop-shared
|
|
104
|
-
"peaks-loop-mut": "0.1.
|
|
105
|
-
"peaks-loop-shared": "0.0.
|
|
103
|
+
"peaks-loop-shared": "0.0.44",
|
|
104
|
+
"peaks-loop-mut": "0.1.17",
|
|
105
|
+
"peaks-loop-shared-channel": "0.0.21"
|
|
106
106
|
},
|
|
107
107
|
"peerDependencies": {
|
|
108
108
|
"@alibaba-group/open-code-review": "1.3.1"
|
|
@@ -102,6 +102,10 @@ Full content extracted to **`references/startup-sequence.md`** (Steps 0 / 0.5-0.
|
|
|
102
102
|
|
|
103
103
|
**Step 0.7 resume rule (read-FIRST):** on resume, `peaks code gate-step-08` reads `progress.json` first and surfaces `Next: slice #N of M (<currentSlice>)` so the orchestrator picks up at the right slice without re-reading the artifact tree.
|
|
104
104
|
|
|
105
|
+
### Peaks-Loop Step 0.51 — `orchestrator-can-do` probe (slice 2026-08-05, lesson-encoded CLI)
|
|
106
|
+
|
|
107
|
+
> **Before deciding to push a slice to the next session**, run `peaks code orchestrator-can-do --slice-spec '<text>' --json`. The command evaluates 4 boundary questions (source code? sub-agent available? requires user decision? context sustainable?) and returns whether the slice can be done in the current session. Default to `canDoInSession=true` unless hard blockers are present; sub-agent dispatch (`peaks sub-agent dispatch rd`) is the canonical delegation path for source-code changes. This encodes the 2026-08-05 lesson — orchestrator MUST NOT Edit/Write `src/` directly, but MUST delegate via sub-agent dispatch. See `.peaks/memory/2026-08-05-peaks-code-orchestrator-capability-misjudgment.md`.
|
|
108
|
+
|
|
105
109
|
### Peaks-Loop Step N+2: Auto-compact at the warning line (v2.13.0 zero-pause contract)
|
|
106
110
|
|
|
107
111
|
> **Zero-pause contract.** When context usage crosses the pre-compact threshold, peaks-loop **automatically fires `peaks compact auto --execute` — the LLM does NOT prompt the user to run `/compact` manually**. Stale prose that says "ask the user to compact" silently stalls the workflow. The v2.13.0 contract makes auto-compact a system responsibility, not a user action.
|
|
@@ -127,6 +131,8 @@ Full content extracted to **`references/startup-sequence.md`** (Steps 0 / 0.5-0.
|
|
|
127
131
|
|
|
128
132
|
If the prose audit (`peaks audit red-lines`) flags any of the above, the slice is **blocked** until the prose is rewritten.
|
|
129
133
|
|
|
134
|
+
See also: references/session-overload-signal-index.md for the 7-signal lookup table the LLM consults before deciding to open a new session.
|
|
135
|
+
|
|
130
136
|
## CLI Drift Index (sediment 2026-07-09)
|
|
131
137
|
|
|
132
138
|
> **Reading guide:** Verified against peaks-loop 4.0.0-beta.6. Each drift below is annotated inline at the relevant step with a `> CLI reality check`. On `error: unknown option ...`, **read the inline reality check first** before guessing.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Session Overload Signal Index
|
|
2
|
+
|
|
3
|
+
> Single-source lookup for the LLM-side "should I open a new session?" decision.
|
|
4
|
+
> Slice 2026-08-05-session-overload-signal-index.
|
|
5
|
+
|
|
6
|
+
## What this doc is
|
|
7
|
+
|
|
8
|
+
When the LLM senses context pressure, it must consult these 7 signals before
|
|
9
|
+
deciding to (a) auto-compact, (b) refuse the next slice, or (c) ask the user to
|
|
10
|
+
open a new session. The contract is: **LLM never re-asks the user about
|
|
11
|
+
cost/length/context** (red line #2 of `references/job-loop.md`). This index is
|
|
12
|
+
the lookup table the LLM must consult in lieu of intuition.
|
|
13
|
+
|
|
14
|
+
## The 7 signals
|
|
15
|
+
|
|
16
|
+
| # | Signal source | Threshold | File / CLI | LLM action |
|
|
17
|
+
|---|---|---|---|---|
|
|
18
|
+
| 1 | prompt size (main session) | 50% / 75% / 80% / 90% | `src/services/context/threshold.ts`; probe via `peaks code context-now` | soft-warn at 50%; consider compact at 75%; MUST NOT auto-compact mid-batch (D6.e); Job mode ≥ 0.85 ⇒ MANDATORY `peaks compact auto --execute` |
|
|
19
|
+
| 2 | auto-compact zone | 0.85 pre-compact / 0.95 red-line | `src/services/code/auto-compact-orchestrator.ts`; `--enforce-job-mode` flag | Auto-fires `peaks compact auto --execute`; LLM MUST NOT prompt user to compact (zero-pause contract v2.13.0) |
|
|
20
|
+
| 3 | sub-agent dispatch prompt size | 50% / 75% / 80% | `src/services/context/context-guard.ts`; `peaks sub-agent-dispatch-guard` PreToolUse hook | soft-warn at 50%; CONTEXT_NEAR_LIMIT at 75%; hard-reject at 80% (CLI + hook double-guard) |
|
|
21
|
+
| 4 | statusline compact bar | visual | `src/services/compact-statusline/compact-statusline-service.ts` | ambient UI; surfaces queued/preparing/compacting/verifying/completed/failed/stalled |
|
|
22
|
+
| 5 | in-flight batch deferral | D6.e | `src/services/code/auto-compact-modes.ts` | defer compact until batch lands (NOT "new session") |
|
|
23
|
+
| 6 | compact stalled | 120s heartbeat timeout | `compact-lifecycle-store.ts` | ambient "stalled" detail in statusline |
|
|
24
|
+
| 7 | sub-agent heartbeat missing | per-role TTL | `peaks sub-agent heartbeat`; `peaks heartbeat-watch` | orchestrator-side warning, not LLM-driven "new session" |
|
|
25
|
+
|
|
26
|
+
## Hard rules
|
|
27
|
+
|
|
28
|
+
- peaks-code SKILL.md red line #2 (see `references/job-loop.md`):
|
|
29
|
+
**Re-ask the user about cost / length / context is forbidden**.
|
|
30
|
+
The LLM MUST NOT prompt the user to open a new session based on overload
|
|
31
|
+
intuition alone; it MUST consult the 7 signals above and prefer auto-compact /
|
|
32
|
+
sub-agent dispatch guard / heartbeat-watch first.
|
|
33
|
+
- "Open a new session" is a **last resort** after auto-compact has been
|
|
34
|
+
attempted and the slice cannot fit. The user's role is to receive the
|
|
35
|
+
handoff capsule, not to micro-manage session lifecycle.
|
|
36
|
+
|
|
37
|
+
## Decision flowchart
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
LLM senses context pressure
|
|
41
|
+
├─► Probe `peaks code context-now --json`
|
|
42
|
+
│ ├─ ratio < 0.50 → continue (no action)
|
|
43
|
+
│ ├─ 0.50 ≤ ratio < 0.75 → soft-warn; continue
|
|
44
|
+
│ ├─ 0.75 ≤ ratio < 0.85 → compact-zone candidate
|
|
45
|
+
│ │ ├─ Job mode → MUST auto-compact now
|
|
46
|
+
│ │ └─ non-Job → wait for next idle turn, then compact
|
|
47
|
+
│ ├─ 0.85 ≤ ratio < 0.95 → pre-compact zone; auto-compact MUST fire
|
|
48
|
+
│ └─ ratio ≥ 0.95 → red-line; synchronous gate (Karpathy §4)
|
|
49
|
+
├─► Check statusline compact bar (signal #4)
|
|
50
|
+
│ ├─ queued / preparing / compacting / verifying → wait
|
|
51
|
+
│ ├─ failed → read failure reason, do NOT spawn new session
|
|
52
|
+
│ └─ stalled (>120s) → surface to user as a blocker, not a new-session ask
|
|
53
|
+
└─► Check sub-agent dispatch guard (signal #3)
|
|
54
|
+
├─ 75% CONTEXT_NEAR_LIMIT → warn, continue with smaller prompt
|
|
55
|
+
└─ 80% PROMPT_TOO_LARGE → hard-reject; refactor the prompt
|
|
56
|
+
|
|
57
|
+
Only after ALL three checks above have been attempted without resolution:
|
|
58
|
+
emit the `peaks session split-handoff` capsule (NOT in this slice; follow-up)
|
|
59
|
+
and the user opens a new session in their own time.
|
|
60
|
+
```
|