@axplusb/kepler 2.2.0 → 2.3.1

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.
@@ -23,6 +23,7 @@ import { calculateCost, formatCostValue, formatTokens, costToCredits, formatCred
23
23
  import { TarangStreamClient, EVENT_TYPES } from '../core/stream-client.mjs';
24
24
  import { JsonlWriter } from '../core/jsonl-writer.mjs';
25
25
  import { createToolExecutor } from '../core/tool-executor.mjs';
26
+ import { buildWorkScope } from '../core/work-scope.mjs';
26
27
  import { CheckpointManager } from '../core/checkpoints.mjs';
27
28
  import { HookRunner } from '../config/hook-runner.mjs';
28
29
  import { runPreflight } from '../onboarding/preflight.mjs';
@@ -47,7 +48,8 @@ import { loadProjectContext } from '../core/project-context-loader.mjs';
47
48
  import { buildContextEnvelope } from '../core/context-envelope.mjs';
48
49
  import { buildResumeHistory, combineResumeSummaries, getRecentSessions, getSessionDetail, getTranscriptProjectRoots } from '../core/local-store.mjs';
49
50
  import { decideResumeMode, projectedTokensForChoice, formatTokens as formatCtxTokens } from '../core/resume-mode.mjs';
50
- import { appendTask, ensureTaskFiles, loadTaskBoard, taskCounts, TASK_FILES } from '../core/tasks.mjs';
51
+ import { appendTask, ensureTaskFiles, loadTaskBoard, moveTask, removeTask, taskCounts, TASK_FILES, updateTask } from '../core/tasks.mjs';
52
+ import { applyCompactSummary, localCompactSummary, parseCompactTailCount, prepareCompactHistory } from '../core/compact-history.mjs';
51
53
  import { toolDisplayLabel, toolDisplaySummary } from './tool-display.mjs';
52
54
  import { createOrbit } from '../state/orbit.mjs';
53
55
  import { attachOrbit, unmount as unmountStatusBar } from '../ui/status-bar.mjs';
@@ -195,6 +197,18 @@ function formatResumeCheckpointStatus(session) {
195
197
  return c.dim(` · summarized${pct}`);
196
198
  }
197
199
 
200
+ function formatResumeContextStatus(session) {
201
+ const full = formatCtxTokens(session?.contextTokens || 0);
202
+ const marker = session?.resumeSummary;
203
+ if (!marker || !Number(marker.sourceMessageCount)) {
204
+ return c.dim(`${full.padStart(5, ' ')} ctx`);
205
+ }
206
+ const resumable = formatCtxTokens(projectedTokensForChoice('checkpoint-full', session.contextTokens || 0, {
207
+ resumeSummary: marker,
208
+ }));
209
+ return c.dim(`${resumable.padStart(5, ' ')} resumable · ${full} full`) + formatResumeCheckpointStatus(session);
210
+ }
211
+
198
212
  function formatRelativeTime(iso) {
199
213
  if (!iso) return '';
200
214
  const t = Date.parse(iso);
@@ -249,7 +263,7 @@ async function pickResumableSession(resumable, ctx) {
249
263
  const ago = formatRelativeTime(s.updatedAt || s.startedAt).padEnd(9, ' ').slice(0, 9);
250
264
  const status = endStatusMarker(s.endStatus);
251
265
  const msgs = String(s.messageCount).padStart(3, ' ') + ' msgs';
252
- const ctx = c.dim(`${formatCtxTokens(s.contextTokens).padStart(5, ' ')} ctx`) + formatResumeCheckpointStatus(s);
266
+ const ctx = formatResumeContextStatus(s);
253
267
  const cost = formatSessionCost(s.costUsd);
254
268
  const partial = s.partial ? c.yellow(' ⚠partial') : '';
255
269
  const instr = oneLineInstruction(s.instruction, 48);
@@ -307,8 +321,14 @@ async function chooseThresholdMode(ctx, decision) {
307
321
  if (rl) rl.pause();
308
322
 
309
323
  const canFull = decision.mode !== 'no-full-allowed';
324
+ const hasCheckpoint = Boolean(decision.resumeSummary?.sourceMessageCount);
325
+ const firstOption = canFull
326
+ ? { key: 'f', value: 'full', label: 'full transcript', enabled: true }
327
+ : hasCheckpoint
328
+ ? { key: 'f', value: 'checkpoint-full', label: 'checkpointed transcript', enabled: true }
329
+ : { key: 'f', value: 'full', label: 'full transcript', enabled: false };
310
330
  const options = [
311
- { key: 'f', value: 'full', label: 'full transcript', enabled: canFull },
331
+ firstOption,
312
332
  { key: 's', value: 'summary', label: 'summary only', enabled: true },
313
333
  { key: '1', value: 'tail-10', label: 'summary + last 10 turns', enabled: true },
314
334
  { key: '2', value: 'tail-20', label: 'summary + last 20 turns', enabled: true },
@@ -327,7 +347,8 @@ async function chooseThresholdMode(ctx, decision) {
327
347
  const projected = formatCtxTokens(decision.projected);
328
348
  const win = formatCtxTokens(decision.windowSize);
329
349
  const lines = [];
330
- lines.push(` This session would use ${c.brand(`${projected} / ${win}`)} tokens (${pct}%)`);
350
+ const rawLabel = hasCheckpoint ? 'Raw full transcript would use' : 'This session would use';
351
+ lines.push(` ${rawLabel} ${c.brand(`${projected} / ${win}`)} tokens (${pct}%)`);
331
352
  if (decision.resumeSummary?.sourceMessageCount) {
332
353
  const covered = Number(decision.resumeSummary.sourceMessageCount) || 0;
333
354
  const full = Number(decision.resumeSummary.fullMessageCount) || 0;
@@ -336,14 +357,18 @@ async function chooseThresholdMode(ctx, decision) {
336
357
  }
337
358
  lines.push(canFull
338
359
  ? ` ${c.yellow('⚠')} ${c.dim('close to the highWatermark — consider a leaner mode:')}`
339
- : ` ${c.red('⛔')} ${c.dim('over hardCap — full mode disabled:')}`);
360
+ : hasCheckpoint
361
+ ? ` ${c.yellow('⚠')} ${c.dim('raw full is over hardCap — checkpoint/tail modes are available:')}`
362
+ : ` ${c.red('⛔')} ${c.dim('over hardCap — full mode disabled:')}`);
340
363
  lines.push('');
341
364
  for (let i = 0; i < options.length; i++) {
342
365
  const o = options[i];
343
366
  const disabled = !o.enabled;
344
367
  const marker = i === selected && !disabled ? c.brand('▸') : ' ';
345
368
  const keyTag = c.dim('[') + (disabled ? c.dim(o.key) : c.brand(o.key)) + c.dim(']');
346
- const proj = formatCtxTokens(projectedTokensForChoice(o.value, decision.projected));
369
+ const proj = formatCtxTokens(projectedTokensForChoice(o.value, decision.projected, {
370
+ resumeSummary: decision.resumeSummary,
371
+ }));
347
372
  const label = disabled ? c.dim(o.label) : (i === selected ? c.brand(o.label) : o.label);
348
373
  const projCol = c.dim(`${proj.padStart(5, ' ')} ctx`);
349
374
  const suffix = disabled ? c.dim(' (over hardCap)') : '';
@@ -388,6 +413,7 @@ async function chooseThresholdMode(ctx, decision) {
388
413
  }
389
414
 
390
415
  function resumeModeLabel(mode = 'full') {
416
+ if (mode === 'checkpoint-full') return 'checkpointed transcript';
391
417
  if (mode === 'summary') return 'summary only';
392
418
  const tailTurns = resumeTailTurnCount(mode);
393
419
  if (tailTurns) return `summary + last ${tailTurns} turns`;
@@ -433,7 +459,7 @@ async function previewResumeSession(session, ctx) {
433
459
  if (scrollOffset > maxOffset) scrollOffset = maxOffset;
434
460
 
435
461
  const lines = [];
436
- lines.push(` ${c.bold('Preview:')} ${c.brand(session.project || '(unknown)')} ${c.dim('Mode:')} ${c.brand(resumeModeLabel(mode))} ${c.dim(formatCtxTokens(projectedTokensForChoice(mode, session.contextTokens)) + ' ctx')}`);
462
+ lines.push(` ${c.bold('Preview:')} ${c.brand(session.project || '(unknown)')} ${c.dim('Mode:')} ${c.brand(resumeModeLabel(mode))} ${c.dim(formatCtxTokens(projectedTokensForChoice(mode, session.contextTokens, { resumeSummary: session.resumeSummary })) + ' ctx')}`);
437
463
  lines.push(` ${c.dim('─'.repeat(60))}`);
438
464
  for (let i = scrollOffset; i < Math.min(scrollOffset + rows, totalLines); i++) {
439
465
  lines.push(fitAnsiLine(` ${c.dim(contentLines[i] || '')}`, cols - 1));
@@ -459,7 +485,7 @@ async function previewResumeSession(session, ctx) {
459
485
  if (key === '') { scrollOffset += 1; render(); return; }
460
486
  if (key === '[5~') { scrollOffset = Math.max(0, scrollOffset - 10); render(); return; }
461
487
  if (key === '[6~') { scrollOffset += 10; render(); return; }
462
- if (low === 'f') { mode = 'full'; history = rich(); scrollOffset = 0; render(); return; }
488
+ if (low === 'f') { mode = session.resumeSummary?.sourceMessageCount ? 'checkpoint-full' : 'full'; history = rich(); scrollOffset = 0; render(); return; }
463
489
  if (low === 's') { mode = 'summary'; history = rich(); scrollOffset = 0; render(); return; }
464
490
  if (low === '1') { mode = 'tail-10'; history = rich(); scrollOffset = 0; render(); return; }
465
491
  if (low === '2') { mode = 'tail-20'; history = rich(); scrollOffset = 0; render(); return; }
@@ -658,6 +684,120 @@ async function summarizeResumeTranscript({
658
684
  }
659
685
  }
660
686
 
687
+ async function compactCurrentSession(ctx, rest = '') {
688
+ const tailCount = parseCompactTailCount(rest, 8);
689
+ const preparedLive = prepareCompactHistory({
690
+ agentHistory: session.agentHistory,
691
+ tailCount,
692
+ });
693
+ if (!preparedLive.ok) {
694
+ process.stderr.write(` ${c.gray(`Nothing to compact — ${preparedLive.reason}.`)}\n`);
695
+ return;
696
+ }
697
+
698
+ const progress = startResumeProgress('compact');
699
+ progress.update('preparing compact summary', 18);
700
+ let sourceMessages = preparedLive.sourceMessages;
701
+ let priorSummary = preparedLive.previousSummary || '';
702
+ let previousSourceMessageCount = 0;
703
+ let fullMessageCount = preparedLive.beforeCount;
704
+ let projectPath = safeCwd();
705
+ let sourceFrom = 'live';
706
+ let summaryWarning = '';
707
+
708
+ try {
709
+ if (session.id && ctx.jsonlWriter?.flush) {
710
+ progress.update('reading transcript checkpoint', 28);
711
+ await ctx.jsonlWriter.flush();
712
+ const detail = await getSessionDetail(session.id, { filePath: ctx.jsonlWriter.transcriptPath });
713
+ if (detail) {
714
+ const richHistory = buildResumeHistory({ ...detail, recapTailTurns: 8 }, `tail-${tailCount}`);
715
+ if (richHistory.sourceMessages?.length) {
716
+ sourceMessages = richHistory.sourceMessages;
717
+ priorSummary = richHistory.priorSummary || '';
718
+ previousSourceMessageCount = richHistory.summaryCheckpointMessageCount || 0;
719
+ fullMessageCount = richHistory.fullMessageCount || sourceMessages.length;
720
+ projectPath = detail.meta?.project || safeCwd();
721
+ sourceFrom = 'transcript';
722
+ } else if (richHistory.priorSummary) {
723
+ priorSummary = richHistory.priorSummary;
724
+ previousSourceMessageCount = richHistory.summaryCheckpointMessageCount || 0;
725
+ fullMessageCount = richHistory.fullMessageCount || preparedLive.beforeCount;
726
+ }
727
+ }
728
+ }
729
+
730
+ if (!sourceMessages.length) {
731
+ progress.stop();
732
+ process.stderr.write(` ${c.gray('Nothing new to compact.')}\n`);
733
+ return;
734
+ }
735
+
736
+ progress.update('summarizing compacted history', 46);
737
+ const backendSummary = await summarizeResumeTranscript({
738
+ auth: ctx.auth,
739
+ toolExecutor: ctx.toolExecutor,
740
+ sessionId: session.id,
741
+ projectPath,
742
+ messages: sourceMessages,
743
+ });
744
+ let summarySource = backendSummary?.summary ? (backendSummary.source || 'backend') : 'local fallback';
745
+ let deltaSummary = backendSummary?.summary || '';
746
+ if (!deltaSummary) {
747
+ summaryWarning = backendSummary?.reason || 'backend summary unavailable';
748
+ deltaSummary = localCompactSummary(sourceMessages);
749
+ }
750
+ const summary = combineResumeSummaries(priorSummary, deltaSummary);
751
+
752
+ progress.update('rewriting live context', 74);
753
+ const applied = applyCompactSummary({
754
+ prepared: { ...preparedLive, sourceMessages },
755
+ summary,
756
+ sessionId: session.id,
757
+ cwd: projectPath,
758
+ originalRequest: session.history.find(m => m.role === 'user')?.content || session.lastTask || '',
759
+ previousSourceMessageCount,
760
+ });
761
+ session.agentHistory = applied.agentHistory;
762
+ session.compactSummary = summary;
763
+ session.compactSourceMessageCount = applied.sourceMessageCount;
764
+
765
+ if (ctx.jsonlWriter) {
766
+ progress.update('writing summary checkpoint', 88);
767
+ ctx.jsonlWriter.writeKeplerEvent({
768
+ type: 'resume_summary',
769
+ data: {
770
+ session_id: session.id || null,
771
+ mode: 'compact',
772
+ mode_label: '/compact',
773
+ summary,
774
+ summary_source: summarySource,
775
+ summary_warning: summaryWarning || null,
776
+ source: sourceFrom,
777
+ source_message_count: applied.sourceMessageCount,
778
+ previous_source_message_count: previousSourceMessageCount,
779
+ full_message_count: fullMessageCount,
780
+ retained_tail_messages: applied.retainedCount,
781
+ live_before_messages: applied.beforeCount,
782
+ live_after_messages: applied.afterCount,
783
+ },
784
+ });
785
+ await ctx.jsonlWriter.flush?.();
786
+ }
787
+
788
+ progress.stop();
789
+ process.stderr.write(
790
+ ` ${c.green('✓')} ${c.dim(`Compacted context: ${applied.beforeCount} → ${applied.afterCount} live messages · retained ${applied.retainedCount} · summary ${summarySource}`)}\n`
791
+ );
792
+ if (summaryWarning) {
793
+ process.stderr.write(` ${c.yellow('⚠')} ${c.dim(`backend summary unavailable — used local summary (${summaryWarning})`)}\n`);
794
+ }
795
+ } catch (err) {
796
+ progress.stop();
797
+ process.stderr.write(` ${c.red(`Compact failed: ${err?.message || String(err)}`)}\n`);
798
+ }
799
+ }
800
+
661
801
  function resumeTailTurnCount(mode = '') {
662
802
  const match = String(mode || '').match(/^tail-(\d+)$/);
663
803
  if (!match) return null;
@@ -1121,12 +1261,26 @@ function printBanner(auth) {
1121
1261
  * Left side: last-turn summary (tools, time, cost)
1122
1262
  * Right side: session totals (ctx%, tokens)
1123
1263
  */
1264
+ function computeCacheTotals() {
1265
+ let read = 0;
1266
+ let write = 0;
1267
+ for (const b of session.costBreakdown) {
1268
+ read += b.cache_read_tokens || 0;
1269
+ write += b.cache_creation_tokens || 0;
1270
+ }
1271
+ const denom = session.inputTokens + read;
1272
+ const hitRate = denom > 0 ? Math.round((read / denom) * 100) : 0;
1273
+ return { read, write, hitRate };
1274
+ }
1275
+
1124
1276
  function buildContextStrip() {
1125
1277
  const totalTokens = session.inputTokens + session.outputTokens;
1126
1278
  const elapsed = formatElapsed(session.startTime);
1279
+ const cache = computeCacheTotals();
1127
1280
 
1128
1281
  const right = [
1129
1282
  c.dim(`${formatTokens(totalTokens)} tok`),
1283
+ ...(cache.read > 0 ? [c.dim(`cache ${cache.hitRate}%`)] : []),
1130
1284
  c.dim(elapsed),
1131
1285
  ].join(c.dim(' · '));
1132
1286
 
@@ -1209,10 +1363,12 @@ function updateStatusBar() {
1209
1363
  // buffered head as a regular two-line shape first so the interleaving
1210
1364
  // content lands below it.
1211
1365
  let _pendingHead = null; // { callId, head, indent }
1366
+ let _lastRenderedBlock = null; // 'tool' | 'content' | null
1212
1367
 
1213
1368
  function flushPendingHead() {
1214
1369
  if (!_pendingHead) return;
1215
- process.stderr.write(`\n${_pendingHead.head}\n`);
1370
+ process.stderr.write(`${_pendingHead.head}\n`);
1371
+ _lastRenderedBlock = 'tool';
1216
1372
  _pendingHead = null;
1217
1373
  }
1218
1374
 
@@ -1297,7 +1453,8 @@ function renderToolResult(data, eventType = 'tool_result') {
1297
1453
  const cols = process.stderr.columns || 120;
1298
1454
  const combined = `${_pendingHead.head} ${outcome}`;
1299
1455
  if (stripAnsi(combined).length <= cols) {
1300
- process.stderr.write(`\n${combined}\n`);
1456
+ process.stderr.write(`${combined}\n`);
1457
+ _lastRenderedBlock = 'tool';
1301
1458
  _pendingHead = null;
1302
1459
  return;
1303
1460
  }
@@ -1311,6 +1468,7 @@ function renderToolResult(data, eventType = 'tool_result') {
1311
1468
 
1312
1469
  // Two-line shape: gutter under the (already-printed or just-flushed) head.
1313
1470
  process.stderr.write(`${gutter}${outcome}\n`);
1471
+ _lastRenderedBlock = 'tool';
1314
1472
 
1315
1473
  // Lint warnings stay visible alongside writes.
1316
1474
  if (hasLint) {
@@ -1433,6 +1591,7 @@ function startContentStream() {
1433
1591
  _streamedPartialText = '';
1434
1592
  _renderedToolResults.clear();
1435
1593
  _renderedContentThisTurn = false;
1594
+ _lastRenderedBlock = null;
1436
1595
  stopSpinner();
1437
1596
  }
1438
1597
 
@@ -1457,12 +1616,14 @@ function flushContent() {
1457
1616
  // Any buffered tool head needs to land BEFORE this content so the order
1458
1617
  // is preserved on screen.
1459
1618
  flushPendingHead();
1619
+ if (_lastRenderedBlock === 'tool') process.stderr.write('\n');
1460
1620
  const rendered = renderMarkdown(_streamBuffer);
1461
1621
  for (const line of rendered.split('\n')) {
1462
1622
  process.stdout.write(` ${line}\n`);
1463
1623
  }
1464
1624
  _streamBuffer = '';
1465
1625
  _renderedContentThisTurn = true;
1626
+ _lastRenderedBlock = 'content';
1466
1627
  }
1467
1628
 
1468
1629
  // ── Event Renderer ──
@@ -1514,11 +1675,13 @@ function renderEvent(event) {
1514
1675
  }
1515
1676
  }
1516
1677
  if (text) {
1678
+ if (_lastRenderedBlock === 'tool') process.stderr.write('\n');
1517
1679
  const rendered = renderMarkdown(text);
1518
1680
  for (const line of rendered.split('\n')) {
1519
1681
  process.stdout.write(` ${line}\n`);
1520
1682
  }
1521
1683
  _renderedContentThisTurn = true;
1684
+ _lastRenderedBlock = 'content';
1522
1685
  }
1523
1686
  break;
1524
1687
  }
@@ -1968,7 +2131,7 @@ function renderPlanOverview({ ctx, mode = 'overview' } = {}) {
1968
2131
  }
1969
2132
 
1970
2133
  renderTaskBoard(board, { showDone: mode === 'status' });
1971
- process.stderr.write(`\n ${c.dim('Update: /tasks add <text> · /tasks active|blocked|done <text>')}\n\n`);
2134
+ process.stderr.write(`\n ${c.dim('Update: /tasks add <text> · /tasks move active 1 done · /tasks edit active 1 <text>')}\n\n`);
1972
2135
  }
1973
2136
 
1974
2137
  function refreshTaskContext(ctx) {
@@ -1987,7 +2150,7 @@ function handleTasksCommand(rest, ctx) {
1987
2150
  process.stderr.write(`\n ${c.bold('Tasks')}\n`);
1988
2151
  process.stderr.write(` ${c.dim('─'.repeat(60))}\n`);
1989
2152
  renderTaskBoard(board, { showDone: true });
1990
- process.stderr.write(`\n ${c.dim('Update: /tasks add <text> · /tasks active|backlog|blocked|done <text>')}\n\n`);
2153
+ process.stderr.write(`\n ${c.dim('Update: /tasks add <text> · /tasks move active 1 done · /tasks edit active 1 <text>')}\n\n`);
1991
2154
  return;
1992
2155
  }
1993
2156
 
@@ -1998,6 +2161,60 @@ function handleTasksCommand(rest, ctx) {
1998
2161
 
1999
2162
  const parts = raw.split(/\s+/);
2000
2163
  let verb = (parts.shift() || '').toLowerCase();
2164
+
2165
+ if (verb === 'move') {
2166
+ try {
2167
+ const [from, index, to, ...textParts] = parts;
2168
+ const result = moveTask({ cwd: safeCwd(), from, index, to, text: textParts.join(' ') || undefined });
2169
+ refreshTaskContext(ctx);
2170
+ process.stderr.write(` ${c.green('✓')} ${c.dim(`moved ${result.from} #${result.index} → ${result.to}`)} ${result.text}\n`);
2171
+ } catch (err) {
2172
+ process.stderr.write(` ${c.red(err.message || String(err))}\n`);
2173
+ process.stderr.write(` ${c.gray('Usage: /tasks move <active|backlog|blocked|done> <number> <active|backlog|blocked|done> [new text]')}\n`);
2174
+ }
2175
+ return;
2176
+ }
2177
+
2178
+ if (verb === 'edit' || verb === 'rename') {
2179
+ try {
2180
+ const [list, index, ...textParts] = parts;
2181
+ const result = updateTask({ cwd: safeCwd(), list, index, text: textParts.join(' ') });
2182
+ refreshTaskContext(ctx);
2183
+ process.stderr.write(` ${c.green('✓')} ${c.dim(`updated ${result.list} #${result.index}`)} ${result.text}\n`);
2184
+ } catch (err) {
2185
+ process.stderr.write(` ${c.red(err.message || String(err))}\n`);
2186
+ process.stderr.write(` ${c.gray('Usage: /tasks edit <active|backlog|blocked|done> <number> <new text>')}\n`);
2187
+ }
2188
+ return;
2189
+ }
2190
+
2191
+ if (verb === 'remove' || verb === 'rm' || verb === 'delete') {
2192
+ try {
2193
+ const [list, index] = parts;
2194
+ const result = removeTask({ cwd: safeCwd(), list, index });
2195
+ refreshTaskContext(ctx);
2196
+ process.stderr.write(` ${c.green('✓')} ${c.dim(`removed ${result.list} #${result.index}`)} ${result.task.text}\n`);
2197
+ } catch (err) {
2198
+ process.stderr.write(` ${c.red(err.message || String(err))}\n`);
2199
+ process.stderr.write(` ${c.gray('Usage: /tasks remove <active|backlog|blocked|done> <number>')}\n`);
2200
+ }
2201
+ return;
2202
+ }
2203
+
2204
+ if (verb === 'finish' || verb === 'complete' || verb === 'block' || verb === 'unblock') {
2205
+ try {
2206
+ const [from, index, ...textParts] = parts;
2207
+ const to = verb === 'block' ? 'blocked' : verb === 'unblock' ? 'active' : 'done';
2208
+ const result = moveTask({ cwd: safeCwd(), from, index, to, text: textParts.join(' ') || undefined });
2209
+ refreshTaskContext(ctx);
2210
+ process.stderr.write(` ${c.green('✓')} ${c.dim(`moved ${result.from} #${result.index} → ${result.to}`)} ${result.text}\n`);
2211
+ } catch (err) {
2212
+ process.stderr.write(` ${c.red(err.message || String(err))}\n`);
2213
+ process.stderr.write(` ${c.gray(`Usage: /tasks ${verb} <active|backlog|blocked|done> <number> [new text]`)}\n`);
2214
+ }
2215
+ return;
2216
+ }
2217
+
2001
2218
  let list = 'backlog';
2002
2219
  if (verb === 'add' || verb === 'new') {
2003
2220
  const maybeList = (parts[0] || '').toLowerCase();
@@ -2192,6 +2409,15 @@ async function handleCommand(input, ctx) {
2192
2409
  }
2193
2410
  process.stderr.write(` ${c.dim('CWD')} ${safeCwd()}\n`);
2194
2411
 
2412
+ // Cache — PRD-071 §1.2. Only surface when we have data; a fresh session
2413
+ // shows nothing rather than a misleading "0%".
2414
+ const cache = computeCacheTotals();
2415
+ if (cache.read > 0 || cache.write > 0) {
2416
+ const readLabel = formatTokens(cache.read);
2417
+ const writeLabel = formatTokens(cache.write);
2418
+ process.stderr.write(` ${c.dim('Cache')} ${cache.hitRate}% hit ${c.dim('·')} ${readLabel} read ${c.dim('·')} ${writeLabel} write\n`);
2419
+ }
2420
+
2195
2421
  // Permissions
2196
2422
  process.stderr.write(`\n ${c.bold('Permissions')}\n`);
2197
2423
  process.stderr.write(` ${c.dim('─'.repeat(44))}\n`);
@@ -2498,10 +2724,7 @@ async function handleCommand(input, ctx) {
2498
2724
  }
2499
2725
 
2500
2726
  case '/compact': {
2501
- const before = session.agentHistory.length || session.history.length;
2502
- if (before <= 4) { process.stderr.write(` ${c.gray('Nothing to compact.')}\n`); return; }
2503
- session.agentHistory.splice(2, session.agentHistory.length - 6);
2504
- process.stderr.write(` ${c.gray(`Compacted agent context: ${before} → ${session.agentHistory.length} messages`)}\n`);
2727
+ await compactCurrentSession(ctx, rest);
2505
2728
  return;
2506
2729
  }
2507
2730
 
@@ -3358,6 +3581,7 @@ export async function startTerminalRepl() {
3358
3581
  if (approval.trustStore) approval.trustStore.policy = effectivePolicy.policy;
3359
3582
  hookRunner.reload();
3360
3583
  latestProjectContext = loadProjectContext({ cwd: safeCwd(), previous: latestProjectContext });
3584
+ const projectResources = toolExecutor.getProjectResources();
3361
3585
  const promptHook = await hookRunner.run('UserPromptSubmit', {
3362
3586
  input: { prompt: input },
3363
3587
  turnId: String(session.turns),
@@ -3379,7 +3603,7 @@ export async function startTerminalRepl() {
3379
3603
  effectivePolicy,
3380
3604
  projectContext: latestProjectContext,
3381
3605
  activeHints: [...hookHints, ...rejectionHints],
3382
- projectResources: toolExecutor.getProjectResources(),
3606
+ projectResources,
3383
3607
  agentContext: toolExecutor.getAgentContext(),
3384
3608
  });
3385
3609
  ctx.effectivePolicy = effectivePolicy;
@@ -3387,6 +3611,14 @@ export async function startTerminalRepl() {
3387
3611
  ctx.latestEnvelope = latestEnvelope;
3388
3612
  Object.assign(execContext, latestEnvelope);
3389
3613
  if (skipPerms) execContext.freeswim = true;
3614
+ // PRD-071: seed work_scope from CLI so the backend has a byte-stable
3615
+ // scope block from turn 1. Uses projectResources already gathered by
3616
+ // the envelope above.
3617
+ execContext.work_scope = buildWorkScope({
3618
+ instruction: input,
3619
+ cwd: safeCwd(),
3620
+ projectResources,
3621
+ });
3390
3622
  for (const file of latestProjectContext.changed || []) {
3391
3623
  if (effectivePolicy.policy.context?.showReloadNotice) {
3392
3624
  process.stderr.write(` ${c.dim(`[Context] ${file.label} updated — re-read`)}\n`);
@@ -7,12 +7,10 @@
7
7
 
8
8
  import { SessionManager } from '../core/session.mjs';
9
9
  import { CheckpointManager } from '../core/checkpoints.mjs';
10
- import { PromptCache } from '../core/cache.mjs';
11
10
  import { readEnv, listEnvVars } from '../config/env.mjs';
12
11
  import * as telemetry from '../telemetry/index.mjs';
13
12
 
14
13
  const checkpoints = new CheckpointManager();
15
- const promptCache = new PromptCache();
16
14
  let sessionManager = null;
17
15
 
18
16
  function getSession() {
@@ -399,11 +397,14 @@ export const COMMANDS = {
399
397
  '/extra-usage': {
400
398
  description: 'Show detailed usage stats',
401
399
  handler(args, state) {
402
- const cacheStats = promptCache.getStats();
403
400
  const telemetryStats = telemetry.getStats();
401
+ const cacheRead = state.tokenUsage.cache_read || 0;
402
+ const cacheWrite = state.tokenUsage.cache_creation || 0;
403
+ const denom = state.tokenUsage.input + cacheRead;
404
+ const rate = denom > 0 ? Math.round((cacheRead / denom) * 100) : 0;
404
405
  return [
405
406
  `Tokens: in=${state.tokenUsage.input}, out=${state.tokenUsage.output}`,
406
- `Cache: hits=${cacheStats.cacheHits}, misses=${cacheStats.cacheMisses}, rate=${cacheStats.hitRate}`,
407
+ `Cache: read=${cacheRead}, write=${cacheWrite}, hit-rate=${rate}%`,
407
408
  `Telemetry: ${telemetryStats.totalEvents} events`,
408
409
  ].join('\n');
409
410
  },