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.')).action(async (opts) => {
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
- * Conservative transcript-size fallback. Walks
23
- * `~/.claude/projects/<hash>/<sid>.jsonl` and estimates
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 `'conservative-fallback'` so callers
27
- * know NOT to treat this as a hard gate.
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
- * Conservative transcript-size fallback. Walks
74
- * `~/.claude/projects/<hash>/<sid>.jsonl` and estimates
75
- * `ratio = bytesUsed / 256K`. Returns the bytes seen so the
76
- * orchestrator can show "estimated from 124KB of 256KB transcript"
77
- * in the envelope. Tagged `'conservative-fallback'` so callers
78
- * know NOT to treat this as a hard gate.
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 readClaudeTranscriptFallback(sessionId) {
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 { readdirSync } = require('node:fs');
86
- for (const hashDir of readdirSync(projectsDir)) {
87
- const candidate = join(projectsDir, hashDir, `${sessionId}.jsonl`);
88
- if (existsSync(candidate)) {
89
- const bytes = statSync(candidate).size;
90
- const ratio = Math.min(1, bytes / (256 * 1024));
91
- return { ratio, bytes };
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: 'conservative-fallback',
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
- * - `conservative-fallback` (transcript size estimate OR
158
- * no signal available — caller
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",
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.6",
102
- "peaks-loop-shared-channel": "0.0.11",
103
- "peaks-loop-shared": "0.0.32"
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"