peaks-loop 4.0.3 → 4.0.4
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
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.4 — 2026-07-31 (Mac auto-compact silent failure + user-driven override)
|
|
4
|
+
|
|
5
|
+
### Bug fixes
|
|
6
|
+
|
|
7
|
+
- **Mac auto-compact silent failure closed** (rid-001-r1, commit `22debcb`): `readClaudeTranscriptFallback` now recurses into `~/.claude/projects/**/<hash>/` (was: flat readdir of `<hash>/<sid>.jsonl`). On Mac Claude Code, where the transcript can sit under a nested layout the previous flat walk could not predict, the probe was returning `ratio: 0` and the auto-compact orchestrator stayed in `none` zone — `/compact` never fired. New source tag `transcript-estimate` distinguishes this real signal from the no-signal `conservative-fallback` so the CLI can label it correctly. No orchestrator threshold change; no new dependency; works on win32 / darwin / linux. Files: `src/services/context/auto-compact-reader.ts` (surgical: +`findTranscriptJsonl` recursive helper), `src/services/context/auto-compact-types.ts` (JSDoc only), `tests/unit/context/auto-compact-reader.test.ts` (new, 6 cases). Acceptance: `peaks code context-now --json` on Mac returns `ratio >= 0.5` when `<sid>.jsonl` is ~200KB. **Plus a 2-line ESM-compatibility fix** (commit `22debcb`): the original `findTranscriptJsonl` used `require('node:fs')` inside the function body, which threw `ReferenceError: require is not defined` under pure Node 22 ESM and was silently swallowed by the outer `try/catch` — vitest's esbuild shim masked the bug. Moved `readdirSync` to top-level import.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- Add: `peaks code context-now --prompt-size <bytes>` user-driven override (rid-002, commit `fa98502`). Mac escape hatch when `CLAUDE_CONTEXT_USAGE_PERCENT` is absent. New `source: 'user-overridden'` value at P0 above env / statusline / transcript / fallback. Example: `--prompt-size 200000` → `ratio ≈ 0.762`, `source: 'user-overridden'`. Files: `src/cli/commands/code-runtime-commands.ts`, `src/services/context/auto-compact-reader.ts` (+P0 short-circuit), `src/services/context/auto-compact-types.ts` (JSDoc), `tests/unit/context/auto-compact-reader.test.ts` (+4 cases).
|
|
12
|
+
|
|
13
|
+
### Documentation
|
|
14
|
+
|
|
15
|
+
- Add: `docs/mac-auto-compact.md` (78 lines) — Mac user-facing verify + escape hatch doc (rid-003, commit `18f4d68`). 5 sections: TL;DR, Why Mac was broken, How to verify, The escape hatch, Hook integration, Caveats, Related.
|
|
16
|
+
- Add: `CLAUDE.md` line 20 callout linking Mac users to the new doc.
|
|
17
|
+
- Add: 2 project memory entries (`.peaks/memory/2026-07-31-mac-auto-compact-{no-env-injection,esm-fake-green-and-fix}.md`) for future maintainer reference (rid-003-r1, commit `03be94f0`).
|
|
18
|
+
|
|
19
|
+
### Sub-package lockstep (4.0.4)
|
|
20
|
+
|
|
21
|
+
- peaks-loop@4.0.4 / peaks-loop-shared@0.0.32 / peaks-loop-mut@0.1.6 / peaks-loop-shared-channel@0.0.11
|
|
22
|
+
- `peaks-loop-shared/dist/version.js` CLI_VERSION bumped to "4.0.4" (publish gate-cli-version reads on-disk, not the packed tarball — both checked)
|
|
23
|
+
|
|
3
24
|
## 4.0.3 — 2026-07-30 (GA release, 4.0.2 tombstone skip)
|
|
4
25
|
|
|
5
26
|
This is the cutover to the in-flight 4.0.3 GA, replacing the prior `## 4.0.1` GA header. 4.0.3 lands because 4.0.2 is a permanent npm tombstone (published-then-unpublished on 2026-07-22; see `.peaks/memory/2026-07-30-4-0-1-published-tombstone-resolution.md` table row 4.0.2: ABSENT in versions[] but time entry `2026-07-22T01:53:48.127Z` still present = tombstoned). The byte-for-byte manifest is identical to 4.0.1; the only diff is the package.json version + the visual README rework (see "Visual / text" below). The 4.0.1 GA section below is preserved as the historical record of the actual `peaks-loop@4.0.1` published at `dist-tags.latest` on 2026-07-30T14:55:22Z; it is NOT the 4.0.3 release body.
|
|
@@ -115,9 +115,20 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
115
115
|
'≥0.95 emits action=red-line (forced hook fires next turn).')
|
|
116
116
|
.requiredOption('--project <path>', 'target project root')
|
|
117
117
|
.option('--session-id <sid>', 'override session id (default: read from active presence)')
|
|
118
|
-
.option('--enforce-job-mode', 'v3.1.2: treat ≥0.85 as MANDATORY auto-compact (not advisory). Auto-enabled when job-shape.json says isJob=true.')
|
|
118
|
+
.option('--enforce-job-mode', 'v3.1.2: treat ≥0.85 as MANDATORY auto-compact (not advisory). Auto-enabled when job-shape.json says isJob=true.')
|
|
119
|
+
.option('--prompt-size <bytes>', 'override the bytes-from-env path; takes priority over env / statusline / transcript. Useful when CLAUDE_CONTEXT_USAGE_PERCENT is absent (e.g. Mac Claude Code).')).action(async (opts) => {
|
|
119
120
|
try {
|
|
120
121
|
const { readContextPercent } = await import('../../services/context/auto-compact-reader.js');
|
|
122
|
+
// rid-002: parse --prompt-size <bytes> defensively. CLI-layer
|
|
123
|
+
// guard rejects non-finite / negative values; only finite
|
|
124
|
+
// non-negative numbers reach the reader. Undefined → no override.
|
|
125
|
+
let promptSizeBytes;
|
|
126
|
+
if (opts.promptSize !== undefined) {
|
|
127
|
+
const parsed = Number(opts.promptSize);
|
|
128
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
129
|
+
promptSizeBytes = parsed;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
121
132
|
// v3.1.2: detect Job mode from job-shape.json when --enforce-job-mode
|
|
122
133
|
// is not explicitly passed. The LLM is the source of truth for
|
|
123
134
|
// whether the request is Job-shaped; the recorded decision is.
|
|
@@ -140,7 +151,8 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
140
151
|
const probe = readContextPercent({
|
|
141
152
|
projectRoot: opts.project,
|
|
142
153
|
sessionId: opts.sessionId ?? readActiveSid(opts.project) ?? 'unknown',
|
|
143
|
-
env: process.env
|
|
154
|
+
env: process.env,
|
|
155
|
+
promptSizeBytes
|
|
144
156
|
});
|
|
145
157
|
const ratioPct = (probe.ratio * 100).toFixed(1);
|
|
146
158
|
let action = 'ok';
|
|
@@ -179,6 +191,7 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
179
191
|
ide: probe.ide,
|
|
180
192
|
capacityBytes: probe.capacityBytes,
|
|
181
193
|
rawBytes: probe.rawBytes ?? null,
|
|
194
|
+
bytesPrompt: promptSizeBytes ?? null,
|
|
182
195
|
capturedAt: probe.capturedAt
|
|
183
196
|
}, [], [
|
|
184
197
|
action === 'red-line'
|
|
@@ -3,6 +3,16 @@ export interface ReadContextPercentInput {
|
|
|
3
3
|
readonly projectRoot: string;
|
|
4
4
|
readonly sessionId: string;
|
|
5
5
|
readonly env?: NodeJS.ProcessEnv | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Slice 2026-07-31-rid-002: explicit byte count from `--prompt-size <bytes>`.
|
|
8
|
+
* When set to a finite non-negative number, short-circuits the entire
|
|
9
|
+
* env / statusline / transcript chain with `source: 'user-overridden'`.
|
|
10
|
+
* Mac escape hatch — Claude Code Mac does NOT inject
|
|
11
|
+
* `CLAUDE_CONTEXT_USAGE_PERCENT` into PreToolUse sub-shells, so the
|
|
12
|
+
* user (or a hook wrapper) can inject the bytes they observed themselves.
|
|
13
|
+
* Priority P0 — above everything else.
|
|
14
|
+
*/
|
|
15
|
+
readonly promptSizeBytes?: number | undefined;
|
|
6
16
|
}
|
|
7
17
|
/**
|
|
8
18
|
* Read env-var-based context percentage. Returns `null` when the env
|
|
@@ -19,12 +29,30 @@ declare function readEnvPercent(env: NodeJS.ProcessEnv, varName: string): number
|
|
|
19
29
|
*/
|
|
20
30
|
declare function readClaudeStatuslinePercent(): number | null;
|
|
21
31
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
32
|
+
* Recursive search for `<sessionId>.jsonl` under `projectsDir`. Used by
|
|
33
|
+
* `readClaudeTranscriptFallback` and exported via `_internal` so unit
|
|
34
|
+
* tests can drive it without monkey-patching `os.homedir` (which is
|
|
35
|
+
* non-configurable in ESM module namespaces).
|
|
36
|
+
*
|
|
37
|
+
* The Mac layout encodes the cwd as a single hash directory; on Mac
|
|
38
|
+
* Claude Code nests the transcript under that hash with an extra level
|
|
39
|
+
* of subdirectory we cannot predict ahead of time. A flat readdir misses
|
|
40
|
+
* that branch and returns null — the silent-failure mode that this fix
|
|
41
|
+
* closes.
|
|
42
|
+
*/
|
|
43
|
+
declare function findTranscriptJsonl(projectsDir: string, sessionId: string): {
|
|
44
|
+
path: string;
|
|
45
|
+
bytes: number;
|
|
46
|
+
} | null;
|
|
47
|
+
/**
|
|
48
|
+
* Conservative transcript-size fallback. Recursively searches
|
|
49
|
+
* `~/.claude/projects/<hash>/<sid-or-nested>.jsonl` (Mac may nest
|
|
50
|
+
* the jsonl under an
|
|
51
|
+
* extra directory we cannot predict ahead of time) and estimates
|
|
24
52
|
* `ratio = bytesUsed / 256K`. Returns the bytes seen so the
|
|
25
53
|
* orchestrator can show "estimated from 124KB of 256KB transcript"
|
|
26
|
-
* in the envelope. Tagged `'
|
|
27
|
-
* know
|
|
54
|
+
* in the envelope. Tagged `'transcript-estimate'` (v2.14.0) so callers
|
|
55
|
+
* know it is a real signal, NOT a hard gate.
|
|
28
56
|
*/
|
|
29
57
|
declare function readClaudeTranscriptFallback(sessionId: string): {
|
|
30
58
|
ratio: number;
|
|
@@ -47,5 +75,6 @@ export declare const _internal: {
|
|
|
47
75
|
readEnvPercent: typeof readEnvPercent;
|
|
48
76
|
readClaudeStatuslinePercent: typeof readClaudeStatuslinePercent;
|
|
49
77
|
readClaudeTranscriptFallback: typeof readClaudeTranscriptFallback;
|
|
78
|
+
findTranscriptJsonl: typeof findTranscriptJsonl;
|
|
50
79
|
};
|
|
51
80
|
export {};
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* 3. `ratio: 0` with `source: 'conservative-fallback'` — the
|
|
23
23
|
* orchestrator MUST NOT auto-fire compact on this signal.
|
|
24
24
|
*/
|
|
25
|
-
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
25
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
26
26
|
import { homedir } from 'node:os';
|
|
27
27
|
import { join } from 'node:path';
|
|
28
28
|
import { detectIdeFromEnv } from './ide-detect.js';
|
|
@@ -70,25 +70,36 @@ function readClaudeStatuslinePercent() {
|
|
|
70
70
|
return null;
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
73
|
+
* Recursive search for `<sessionId>.jsonl` under `projectsDir`. Used by
|
|
74
|
+
* `readClaudeTranscriptFallback` and exported via `_internal` so unit
|
|
75
|
+
* tests can drive it without monkey-patching `os.homedir` (which is
|
|
76
|
+
* non-configurable in ESM module namespaces).
|
|
77
|
+
*
|
|
78
|
+
* The Mac layout encodes the cwd as a single hash directory; on Mac
|
|
79
|
+
* Claude Code nests the transcript under that hash with an extra level
|
|
80
|
+
* of subdirectory we cannot predict ahead of time. A flat readdir misses
|
|
81
|
+
* that branch and returns null — the silent-failure mode that this fix
|
|
82
|
+
* closes.
|
|
79
83
|
*/
|
|
80
|
-
function
|
|
81
|
-
const projectsDir = join(homedir(), '.claude', 'projects');
|
|
84
|
+
function findTranscriptJsonl(projectsDir, sessionId) {
|
|
82
85
|
if (!existsSync(projectsDir))
|
|
83
86
|
return null;
|
|
84
87
|
try {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
const stack = [projectsDir];
|
|
89
|
+
while (stack.length > 0) {
|
|
90
|
+
const dir = stack.pop();
|
|
91
|
+
if (dir === undefined)
|
|
92
|
+
break;
|
|
93
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
const full = join(dir, entry.name);
|
|
96
|
+
if (entry.isDirectory()) {
|
|
97
|
+
stack.push(full);
|
|
98
|
+
}
|
|
99
|
+
else if (entry.isFile() && entry.name === `${sessionId}.jsonl`) {
|
|
100
|
+
const bytes = statSync(full).size;
|
|
101
|
+
return { path: full, bytes };
|
|
102
|
+
}
|
|
92
103
|
}
|
|
93
104
|
}
|
|
94
105
|
}
|
|
@@ -97,6 +108,24 @@ function readClaudeTranscriptFallback(sessionId) {
|
|
|
97
108
|
}
|
|
98
109
|
return null;
|
|
99
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Conservative transcript-size fallback. Recursively searches
|
|
113
|
+
* `~/.claude/projects/<hash>/<sid-or-nested>.jsonl` (Mac may nest
|
|
114
|
+
* the jsonl under an
|
|
115
|
+
* extra directory we cannot predict ahead of time) and estimates
|
|
116
|
+
* `ratio = bytesUsed / 256K`. Returns the bytes seen so the
|
|
117
|
+
* orchestrator can show "estimated from 124KB of 256KB transcript"
|
|
118
|
+
* in the envelope. Tagged `'transcript-estimate'` (v2.14.0) so callers
|
|
119
|
+
* know it is a real signal, NOT a hard gate.
|
|
120
|
+
*/
|
|
121
|
+
function readClaudeTranscriptFallback(sessionId) {
|
|
122
|
+
const projectsDir = join(homedir(), '.claude', 'projects');
|
|
123
|
+
const hit = findTranscriptJsonl(projectsDir, sessionId);
|
|
124
|
+
if (hit === null)
|
|
125
|
+
return null;
|
|
126
|
+
const ratio = Math.min(1, hit.bytes / (256 * 1024));
|
|
127
|
+
return { ratio, bytes: hit.bytes };
|
|
128
|
+
}
|
|
100
129
|
/**
|
|
101
130
|
* Probe the current AI CLI's context-fill ratio. Adapter-driven:
|
|
102
131
|
* looks up the registered `IdeAdapter.compact` profile via
|
|
@@ -121,6 +150,24 @@ export function readContextPercent(input) {
|
|
|
121
150
|
// through `unknown` to IdeId's wider 8-element set.
|
|
122
151
|
const ideId = (detected === 'unknown' ? 'claude-code' : detected);
|
|
123
152
|
const adapter = getAdapter(ideId);
|
|
153
|
+
// P0 user-overridden takes priority over env / statusline / transcript.
|
|
154
|
+
// Mac escape hatch: when the CLI/helper passes `--prompt-size <bytes>`,
|
|
155
|
+
// honor that number directly. Do NOT read env, statusline, or transcript
|
|
156
|
+
// — user intent always wins. Negative / non-finite values are ignored
|
|
157
|
+
// here (CLI layer validates >= 0); they fall through to the chain below.
|
|
158
|
+
if (input.promptSizeBytes !== undefined &&
|
|
159
|
+
Number.isFinite(input.promptSizeBytes) &&
|
|
160
|
+
input.promptSizeBytes >= 0) {
|
|
161
|
+
const ratio = Math.min(1, input.promptSizeBytes / capacityBytes);
|
|
162
|
+
return {
|
|
163
|
+
ratio,
|
|
164
|
+
source: 'user-overridden',
|
|
165
|
+
rawBytes: input.promptSizeBytes,
|
|
166
|
+
capacityBytes,
|
|
167
|
+
ide: ideId,
|
|
168
|
+
capturedAt
|
|
169
|
+
};
|
|
170
|
+
}
|
|
124
171
|
// Primary: read the adapter-declared env-var (no hard-coded IDE names).
|
|
125
172
|
if (adapter.compact) {
|
|
126
173
|
const primary = readEnvPercent(env, adapter.compact.envVarForContextPercent);
|
|
@@ -147,7 +194,7 @@ export function readContextPercent(input) {
|
|
|
147
194
|
if (fallback !== null) {
|
|
148
195
|
return {
|
|
149
196
|
ratio: fallback.ratio,
|
|
150
|
-
source: '
|
|
197
|
+
source: 'transcript-estimate',
|
|
151
198
|
rawBytes: fallback.bytes,
|
|
152
199
|
capacityBytes,
|
|
153
200
|
ide: ideId,
|
|
@@ -161,4 +208,4 @@ export function readContextPercent(input) {
|
|
|
161
208
|
return { ratio: 0, source: 'conservative-fallback', capacityBytes, ide: ideId, capturedAt };
|
|
162
209
|
}
|
|
163
210
|
/** Re-export the env-var probe for unit tests. */
|
|
164
|
-
export const _internal = { readEnvPercent, readClaudeStatuslinePercent, readClaudeTranscriptFallback };
|
|
211
|
+
export const _internal = { readEnvPercent, readClaudeStatuslinePercent, readClaudeTranscriptFallback, findTranscriptJsonl };
|
|
@@ -152,10 +152,18 @@ export interface ContextPercentProbe {
|
|
|
152
152
|
readonly ratio: number;
|
|
153
153
|
/**
|
|
154
154
|
* Adapter-driven source tag. Fixed values:
|
|
155
|
+
* - `user-overridden` (v2.14.0 rid-002: explicit `--prompt-size
|
|
156
|
+
* <bytes>` from the CLI / hook wrapper —
|
|
157
|
+
* takes priority P0 over EVERY other source.
|
|
158
|
+
* Mac escape hatch.)
|
|
155
159
|
* - `${ideId}-env` (e.g. `claude-code-env`)
|
|
156
160
|
* - `statusline-poll` (Claude Code MVP fallback)
|
|
157
|
-
* - `
|
|
158
|
-
*
|
|
161
|
+
* - `transcript-estimate` (v2.14.0 Mac-aware: recursive
|
|
162
|
+
* readdir under ~/.claude/projects/
|
|
163
|
+
* finds <sid>.jsonl and reports
|
|
164
|
+
* bytes/256K; a real signal, NOT
|
|
165
|
+
* a hard gate)
|
|
166
|
+
* - `conservative-fallback` (no signal available — caller
|
|
159
167
|
* MUST NOT treat as hard gate)
|
|
160
168
|
* Future IDEs may add per-ide sources; the type accepts any
|
|
161
169
|
* string to keep the schema forward-compatible.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-loop",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.4",
|
|
4
4
|
"description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
|
|
5
5
|
"author": "SquabbyZ",
|
|
6
6
|
"keywords": [
|
|
@@ -98,9 +98,9 @@
|
|
|
98
98
|
"headroom-ai": "0.22.4",
|
|
99
99
|
"yaml": "^2.9.0",
|
|
100
100
|
"zod": "^3.25.76",
|
|
101
|
-
"peaks-loop-mut": "0.1.
|
|
102
|
-
"peaks-loop-shared
|
|
103
|
-
"peaks-loop-shared": "0.0.
|
|
101
|
+
"peaks-loop-mut": "0.1.7",
|
|
102
|
+
"peaks-loop-shared": "0.0.33",
|
|
103
|
+
"peaks-loop-shared-channel": "0.0.12"
|
|
104
104
|
},
|
|
105
105
|
"peerDependencies": {
|
|
106
106
|
"@alibaba-group/open-code-review": "1.3.1"
|