feat(cli): print the composed first-turn prompt sizes per section (--print-prompt-sizes)

CO
3h ago
+1750-45

Fixes #3483. Part of #3460.

What

senpi --print-prompt-sizes prints the per-section size breakdown of the request this session would send on its first turn in the current directory. It prints sizes only (section name, source, bytes, estimated tokens, the bounding setting) and never prompt content. It makes no model call and no network request. Sections are sorted biggest first, with a total.

One path, not a parallel counter

  • dynamic-prompt/build.ts now produces ordered named parts (core, tuning, per-context-file project context, skills, cwd, and so on), and the base prompt is their exact join. agent-session builds custom and appended prompts through the same assembly.
  • extensions/runner.ts adds a preview-only observer to before_agent_start, so the bytes each extension adds (memory projection, rules, terminal, ...) are attributed to it.
  • prompt-cache-prefix-request.ts reports sizes from the same preview and provider-context build the first turn uses. A test checks that the sum equals the composed system prompt plus the tool declarations.
  • senpi #3137/#3154 (projected-context admission) has no per-section accounting, so there was nothing to reuse (noted in changes.md).

Byte identity (prompt-cache safety)

Golden fixtures were captured from main 89ecd50ca0 before the refactor (commit 0a05d18aee). Seven option sets and a full memory-projection-style extension prefix stay byte-identical. A real omo -p first-turn before/after comparison follows in a PR comment.

Evidence (local, exact head)

  • RED first: 12 failures before the implementation.
  • GREEN: 25 tests passed across 4 files, including the existing prefix-preview suites.
  • bun run check: exit 0. Changelog gate: PASS.
  • A real run in a temp project measured 21,361 system-prompt bytes + 25,139 tool-declaration bytes = 46,500 bytes, matching the raw-prefix dump exactly.

Summary by cubic

Adds senpi --print-prompt-sizes, which prints a sizes-only JSON per-section breakdown (name, source, bytes, estimated tokens, bounding settings) of the composed first-turn system prompt and tool declarations for the current project, without a model call or prompt content. Fixes #3483.

  • Refactors dynamic-prompt building into ordered named parts whose joined output is byte-identical to main (golden fixtures), preserving prompt-cache hits. buildDynamicSystemPrompt now honors customPrompt/appendSystemPrompt; callers of the preset-only path must strip these fields to avoid double-append.
  • Sizes-only startup skips only MCP attach and cache-keepalive prewarm; all other session_start handlers run so the report matches a real first turn. MCP servers are listed by name/count, third-party hooks are named with a warning, and colliding extension names are disambiguated with hashed paths.
  • Exits nonzero when no model is selected, session initialization fails, or an unsafe preview handler would force an approximate result. A preset whose parts don't join to its prompt is reported under presetPartsMismatch instead of falling back.
  • Filesystem lookups for package names and MCP config are injected only via the CLI's ExtensionBindings.promptSizeInventory, keeping sync reads off the shared RPC session path.