SLOPSHOPPER

draft

Writing to a length in one draft and cuts, not rewrites. When your prompt sets a word limit, it carries your setting to plan each part's share and land the…

guardprompt
v0.1.3MITupdated 2026-10-18HarzerHeribert/claude-modpack/draft
A shopper browsing a rack in a slop shop
README

<img alt="claude-modpack: eight Claude Code mods, each measured on real tasks before it shipped, built to work together. statusline, smart-compact, delegate, cells, eta, seek, draft, route." src="assets/banner-light.svg" width="880">

<h3>Install in Claude Code</h3>

/plugin install modpack --marketplace HarzerHeribert/claude-modpack

Answer y, then run /modpack:setup to set everything up step by step.<br> Or from your terminal, guided through both steps: bash <(curl -fsSL https://raw.githubusercontent.com/HarzerHeribert/claude-modpack/main/install.sh) (read it first).<br> Prefer doing it by hand? SETUP.md has every step.

Release License: MIT Claude Code 2.1.296 Quality: no loss seen 8 mods Lab notes

Claude Code with fewer wasted steps, and a clear view of what it's doing.

Eight free add-ons that work on their own. One command installs them, and there is nothing new to learn.<br> Every number on this page was measured, including where it doesn't help.

Install · What you get · Honest numbers · The site: ideas and evidence · Lab notes · Questions

<img alt="Animated: the cost per job with and without the pack, as bars that grow: in a long session $0.44 against $0.29 (−33%), bigger jobs in a fresh session $0.55 against $0.51 (−8%), quick questions in a fresh session $0.23 against $0.24 (+2%, about even); then what was measured since: −17% on 12 jobs beyond code with the pack as 0.7.0 ships it, −22% on a feature a cheaper model builds from your plan, −11% writing to a word limit in one draft, and compacting a 70k-token session in 4 ms instead of 15 s with the summary written ahead; then 2,550 graded runs, no quality loss seen." src="assets/numbers.gif" width="880">

Why it saves

Every time Claude takes a step (reads a file, makes an edit, runs the tests), it reads your whole conversation again. That re-reading is most of what a session costs, and it grows the longer you work. So the cheapest step is the one Claude never has to take.

The pack takes out steps that need no thinking. Several edits and their test run as one step. A search for a name in your code comes back with the exact answer. And when you approve a plan, a cheaper model does the building while Claude checks the result. The work comes out the same; Claude just gets there in fewer round trips.

What you get

<img alt="A real Claude Code session with the pack, six moments cut together, each with a caption: a bar and a finish time for long jobs (eta); several edits and the test run in one step (cells); an approved plan built by a cheaper model and checked (delegate); asked for at most 300 words, one draft that fits (draft); compacted at once, its summary written ahead, with that setting on (smart-compact); every mod with a switch that acts at once (/mods). Under the prompt throughout, the status line: model, context, the plan's 5-hour and weekly use." src="assets/demo-tour.gif" width="880">

Always know where your plan stands

One line under the prompt: the model, how full the conversation is, and how much of your plan's 5-hour and weekly limits you have used, with when each one resets. Long jobs get a progress bar and a finish time above the prompt. And when you step away, your session is kept ready, or tidied up before it goes cold, whichever costs less; the line tells you which.

These are the statusline, eta and smart-compact mods.

No waiting when a conversation gets long

When a conversation fills up, Claude Code summarizes it so the work can go on, and you wait while it writes: 15 seconds at 67k tokens and 102 seconds at 436k in our sessions. Turn on compacting after a turn (afterTurnPayback) and smart-compact compacts a long conversation shortly after a turn ends, when that pays back soon. Its summary is written in the background as the turn ends, so the compaction itself takes 3 to 5 ms. Against /compact from the same session, the next job cost 14% less, at full marks in 16 of 18 runs against 14 of 18 (lab note 15).

That's the smart-compact mod (compacting after a turn is off until you turn it on).

Several changes in one step, and you can still undo them

Claude makes several edits and runs the tests as one step, and you watch each part happen in a live card. Every edit still goes through Claude Code's own edit tool: you see each change, and /rewind undoes it.

That's the cells mod.

A cheaper model builds what you approved

When you approve a plan, Claude hands the building to Sonnet, a cheaper model, and checks its work before reporting back. In our tests that made a feature 22% cheaper and new tests 26% cheaper, with every result correct (corrected 16 Oct: worker costs were undercounted; it was 27% and 33%). You can watch the helper work in its card, and /delegate shows every step it took.

That's the delegate mod.

Optional: feature work goes to the cheaper model by itself

An optional mod, off until you turn it on, and the one that needs an outside classifier model: an API key for TypeSafe's Jev or ScaleDown. That classifier reads each prompt you send. Only when it is feature or test work is Claude told to hand it straight to delegate's worker: the worker reads the code and builds, delegate checks the result itself, and Claude reports after two steps of its own. On a TypeScript feature that made the job 51% cheaper and 42% faster than the pack alone, with every result correct (corrected 16 Oct: worker costs were undercounted; it was 57%); everything else stays with Claude. The classifier is an outside service (TypeSafe's Jev or ScaleDown), so this is off until you turn it on and give it a key: from then on, the start of each prompt goes to it, in the US, to be classified. In real sessions it saves less than on the benchmark: in the author's, the turns it would hand off were 13% of the spend, so at most 4 to 7% saved, nearer 3 to 6% with the hand-offs that were not feature or test work; more if most of your work is building features, and less if you already plan in plan mode, where delegate hands an approved plan to the worker without route.

That's the route mod (optional; it needs an outside classifier and its key).

Fewer, leaner steps when Claude looks around

Before Claude changes anything, it looks around your code, and everything it reads stays in the conversation, read again with every later step. Beside the first prompt you send in a session, and again as the session grows, the pack passes on your preference to look around in few, lean steps: the searches it can already name in one go, the lines that matter rather than whole files, nothing read twice. Against the pack without it, that made jobs 12% cheaper in a fresh session, 16% on a TypeScript library and 10 to 16% in long sessions, at the same quality. It runs on your machine and sends nothing anywhere.

That's the seek mod.

Writing to a word limit, in one draft

Ask for a text of at most so many words, and Claude tends to write all of it, count, find it a third too long, and write all of it again, several times. With a limit in your prompt, the pack passes on your preference to give each part its share and aim a little under, to keep every fact you asked for (and say so if they cannot all fit), and after each write it shows the word count and, when the text is well over, each section's target, so one more write lands it. On six writing tasks that made them 11% cheaper at the same quality. Without a word limit in your prompt it does nothing.

That's the draft mod.

You stay in control

/mods lists the eight and turns any of them on or off, right away, with a click or by typing, such as /mods statusline off. When something can't fully work on your computer, the pack tells you once, with the command that fixes it. When another plugin's permission or notification hook hangs (Warp's does outside the Warp terminal), the pack stops waiting for it after 10 seconds, so Claude Code can't freeze, and tells you which one it was. It keeps itself up to date (or only lets you know, if you prefer).

That's the modpack plugin, which installs the other eight.

Install

In Claude Code, type:

/plugin install modpack --marketplace HarzerHeribert/claude-modpack

Answer y to add the marketplace, then pick where to install it. That installs all eight. Then, in the project you work in:

/modpack:setup

It checks your machine, asks once what you want on (route stays off unless you say yes), sets each mod up in order, and checks that each one works. Done. /mods shows what's on.

By hand instead: SETUP.md has every step.

Each mod installs on its own the same way, for example /plugin install smart-compact --marketplace HarzerHeribert/claude-modpack. delegate and cells use a language server where you have one installed and work without it: Language servers.

Honest numbers

We measured the pack on Claude Opus 5.5 doing seven real jobs in an open-source project (click), from a one-line lookup to building a feature, on two days, 13 and 15 October. Every result was checked by hidden tests. The same jobs ran with the pack and without it, and we compared what they cost at API prices.

Cost per jobWithoutWith the pack
In a long session (Claude has already read the whole project)$0.44$0.29−33%
Bigger jobs in a long session (a feature, tests, a rename)$0.91$0.57−37%
In a fresh session$0.37$0.35−4%
Bigger jobs in a fresh session$0.55$0.51−8%, and 15% faster
Quick questions in a fresh session$0.23$0.24+2%, about even
  • Where it doesn't help: quick questions in a brand-new session. The pack's tools come along with every step, and a three-step lookup has little to save: 5% more one day, 4% less the next, about even.
  • Writing tests: the pack alone saves nothing there (−2%, 12 runs each way): every step waits on the last test run. route hands it to the worker instead (−36% to −46%, below).
  • Nothing got worse: all 224 runs behind this table produced a correct result, with the pack and without.
  • The 95% ranges: long session −39% to −27%; fresh −7% to −1%; bigger jobs fresh −12% to −3%; quick questions −1% to +6%. The long session is two runs per job each day, and the days differ: −26% on one, −40% on the other, mostly from the feature (plain needed 12 requests one day, 16 the next).
  • The same on other code: on a TypeScript library (chrono) the pack cost 7.6% less (−12% to −3%) and took fewer steps on all eight jobs. On 30 real bug reports from SWE-bench Verified it fixed 30 against 29, with 23% fewer steps; the cost, −12%, is not certain at one attempt per bug (−33% to +12%). On PocketBase, Trail-Code's own 15 tasks: −21% with 0.6.0, at one run per task as Trail-Code ran them (the average task −18%, between −27% and −8%; lab notes 3).
  • Beyond click, new in 0.7.0: on 12 tasks in a React and TypeScript app, a small service to run and fix, and documents and data to write and analyse, the pack as 0.7.0 ships it cost 17% less than plain Claude Code (−22% to −12%, 48 runs, all but four at full marks): web code −28%, operating software −8%, prose and data −5% (−10% to +1%), so prose is where it does least.
  • A plan handed to a cheaper model: −22% on a feature, −26% on tests, against Opus doing it alone with the pack (16 runs, all correct).
  • With route turned on: a further −14% on click's eight jobs (route 0.1). With 0.6.0's verified hand-offs, against the pack in the same batch on TypeScript: a feature −51% and 42% faster, writing tests −36%; Claude itself takes 2 steps instead of 6 to 11 (24 runs, all correct).
  • Corrected 16 Oct: worker costs were undercounted. A worker's transcript records its output as each response began, and our bench counted it from there, so every number with a hand-off in it came out too good. Corrected from Claude Code's own totals: a planned feature −22% (was −27%), tests −26% (was −33%), route on click −14% (was −17%), the TypeScript feature −51% (was −57%). Every saving is still a saving, a smaller one. The table above has no hand-offs and did not change. Each claim, old and new: CORRECTION-1016.md.
  • With seek (new in 0.6.0, measured against the pack rather than plain Claude Code, so not in the table above): −12% fresh on click, −16% on TypeScript, −10% to −16% in long sessions (seek, below).
  • With draft (new in 0.8.0, measured against the pack): −10.7% on six writing tasks, from −14.9% to −6.3%, at the same quality (0.987 against 0.990), and no run over its word limit (draft, below).
  • Two days, two versions: the 13 October runs used the pack as of 0.3.0 and Claude Code 2.1.296, the 15 October runs 0.4.0 and 2.1.299. What changed in the pack between them (on/off switches, setup, messages shown when a worker is refused) never reached the model in these runs: its first request was 26.7k tokens plain and 30.6k with the pack on both days. The second day's plain runs were done twice, because the first try had the pack installed by mistake: lab notes 8 tells how we caught it.

<img alt="Cost per task, plain Claude Code against the modpack, in a fresh session and in a session already holding 216k tokens." src="assets/cost-light.svg" width="880">

<img alt="Model requests per task, plain Claude Code against the modpack: 3.2 against 5.6 in a session already holding 216k tokens." src="assets/requests-light.svg" width="880">

<img alt="Cost per task, Opus alone against a plan handed to a worker: feature work 22% less, tests 26% less." src="assets/delegate-light.svg" width="880">

How we measured, every run's record, and the scripts to run it all again are in benchmarks/, starting with SUITE.md. What we tried that did not make it is in the lab notes (as Markdown), among it a 10,000-star code graph, Trail-Code, which installed into Claude Code saved nothing we could measure: alone −1.8% against plain Claude Code (−5.9% to +2.7%), beside the pack +13% (lab notes 3). Not measured: what smart-compact saves you while you're away (worked out from prices, not tested) and Windows.

Questions

Will Claude's work get worse? Not that we have seen. Every run is graded by hidden tests or fixed rules, and the same jobs run with the pack and without it. In the batches that did both, 6 of 349 runs without the pack and 8 of 407 with it fell short of full marks. Of all 2,586 graded runs that ship with this release, 96 fell short: seven on one TypeScript explanation whose answers sometimes leave out one term of its list, with the pack and without; five with Trail-Code, whose hook replaced the answer; seven with Haiku as the main model, in two experiments we dropped; one TypeScript type question; and six on PocketBase. There, two questions left out terms of their answer key in both arms, and plain Claude Code missed one term of a third. On that third question the pack scored 0: lsp-plus held a search while Go's language server was still loading, Claude Code moved it to the background, and Claude's last message answered the notice that followed instead of the question. lsp-plus 0.4.2 then gave the language server 8 seconds at most, and lsp-plus has since left the pack. Seven more are 0.7.0's: five on one prose revision whose checks are strict, in every arm, plain Claude Code too; one runbook and one rename. And 22 are 0.8.0's: nine in the writing tasks, three with draft and three without in one batch and three more with draft in the next (a removed command named as current, a follow-up missing from a brief); thirteen in the compaction benchmark, spread over every arm, ten of them one option check of one task and three a rename that kept a deprecated alias of the old name. Six are 0.9.0's, from the benchmark of the summary written ahead against /compact: five that same option check, two with the summary written ahead and three with /compact, and one rename under /compact that left the old name in one file. And 29 ship with the lab notes of 19 October: 28 from draft's first writing batches, with draft and without, graded before three of their graders were fixed (a removed command named in a history sentence, a code-span check, a stale base; lab note 13), and one with steer, a system-prompt experiment we did not ship. And six ship with 0.9.3, from the next-task benchmark of two shorter summaries, three each: the same option check three times, and the rename three times (twice a docstring note naming the old name, once the old name left in one file). Apart from the PocketBase run, the pack changes how many steps Claude takes, not whether it gets there.

Where did lsp-plus go? It left the pack in 0.7.0. Measured twice, 120 runs on renames and traces in click, a TypeScript library and a React app and on web, operating and prose work, the pack cost no more without it (−1.8% and −1.6%, both ranges across zero), and it kept a language server running per language in every session, hundreds of MB each. Its answer to a grep came from the language server, and in untyped code a server finds about half the uses of a name, where a plain word search finds them all (lab notes 12). The update removes it once and says so in one line; installed again on purpose, it stays. Its last version, 0.4.2, is in this repository's v0.6.1 tag.

Is it safe? It's open source (MIT), so every line is here to read. Code that Claude runs inside a cell runs in a sandbox with no network access. Edits go through Claude Code's own edit tool, so your usual permission prompts and /rewind still apply. The pack sends nothing anywhere except a once-a-day check on GitHub for a new release, which you can turn off, and, only if you turn route on, the start of each prompt to the classifier you chose (TypeSafe or ScaleDown).

Does the pack add anything to my prompts? Your prompt itself is never changed. Beside it, Claude can get a short note in your name: seek's setting to look around in few, lean steps (with the first prompt of a session, and again after a compaction), and, only if you turn route on, the order to hand feature or test work to the worker; delegate gives the same order when you approve a plan of 20 lines or more. A subagent Claude starts gets cells' and seek's settings at the end of its task, since your prompts never reach it. /mods seek off and /mods route off stop theirs; subagents: false in cells or seek stops theirs for subagents.

I'm on a Pro or Max plan. Does this help me? You don't pay per job, but every step counts against your 5-hour and weekly limits. We measured cost at API prices, not plan usage, so we can't promise a number. Fewer, lighter steps should leave more of your limits, and the status line shows exactly where you stand.

Does it work in the desktop app? Yes, cards, progress and the status line included. One limit: in auto mode the desktop app doesn't let an add-on start a helper model, so plans aren't handed off there (turn route off there too); in manual mode, or in the terminal, they are. Windows hasn't been tested yet.

How do I turn it off? /mods switches each part off or on, right away. /plugin removes them.

Were any of these numbers wrong? Yes, the hand-off ones. On 16 October we found that our bench had counted a worker's output only as each of its responses began, so every saving that involved a worker came out too large: a planned feature was −27% and is −22%. They are corrected here, the old and new number side by side in CORRECTION-1016.md. The pack-against-plain numbers had no workers and stand.

What does it cost? Nothing. It's free and open source.

The mods in depth

One line under the prompt, drawn in Claude Code's own hint row so the mode indicator and the status share it:

⏵⏵ auto mode on       Opus 5.5 xhigh   412k ━━━━━━──────   5h 34% ↺2h14   7d 12% ↺3d   ● ready · cold in 44m
  • Its own segments: the model and its effort, the context against the auto-compact point (a highlight sweeps along it while Claude works), and how much of the plan's 5-hour and 7-day windows is used, with when each resets: quiet under half, plain past it, amber from 80%.
  • Other mods' segments: a mod calls $.statusline.set({ id, priority, parts, short?, side? }) and lists statusline among its dependencies. A part is text, a countdown ({ until, prefix?, warnMs? }, counted by the line) or a spinner ({ spin: true }), so a mod sends a segment when it changes, not every second. $.statusline.remove({ id }) takes it off.
  • Narrow terminals drop the least important first: the model, then the windows' resets, the windows, the gauge, and another mod's countdown before its own words.
  • Published pages: a pill on the left lists the pages this session published with the Artifact tool.
  • Phone and IDE: where no line is drawn, the same line is the session's status text, in a session the phone or IDE hosts. A terminal session watched over Remote Control (/rc) from the desktop app, the web or the phone gets no mod drawing and no status text as of Claude Code 2.1.296.
OptionDefault
enabledtruefalse turns the mod off; /mods does it for all of the pack.
placementlinetail: appended dim to Claude Code's hint. band: above the prompt. off.
limitsalwayshalf: a plan window only once it is half used. off: never.
animatetrueThe highlight along the gauge.

An idle session's prompt cache expires after its TTL: 1 hour on a subscription, 5 minutes in ove

Source 2 files
hooks/register.ts 54 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3import { PROSE, gaugeText, limitOf, orderText } from './length'
4
5type Engine = EngineInterface
6
7/** The person's own prompts: typed, from the phone, or a `claude -p` turn. */
8const PERSON = new Set(['composer', 'bridge', 'sdk'])
9const WRITES = ['Write', 'Edit', 'MultiEdit'] as const
10
11// The last word limit the person set in this session: a later prompt with a limit replaces it.
12let limit: number | undefined
13let ordering = true
14let gauging = true
15
16type Result = { deny?: string; isError?: boolean; context?: string[] }
17
18const relative = (cwd: string, path: string) => (path.startsWith(`${cwd}/`) ? path.slice(cwd.length + 1) : path)
19const absolute = (cwd: string, path: string) => (path.startsWith('/') ? path : `${cwd}/${path}`)
20
21async function gauge($: Engine, path: string): Promise<string | undefined> {
22  if (limit === undefined || !PROSE.test(path)) return undefined
23  const cwd = await $.session.cwd()
24  const text = await $.fs.read(absolute(cwd, path)).catch(() => undefined)
25  return typeof text === 'string' ? gaugeText(relative(cwd, path), text, limit) : undefined
26}
27
28export const register: Register = (on, options) => {
29  if (options.enabled === false) return
30  limit = undefined
31  ordering = options.order !== false
32  gauging = options.gauge !== false
33
34  // A prompt that sets a word limit: the limit is kept for the gauge, and the setting rides beside it.
35  on('prompt.submit', async ($, e, next) => {
36    if (!PERSON.has(e.origin?.kind as string) || e.text.trim().startsWith('/')) return next(e)
37    const found = limitOf(e.text)
38    if (found === undefined) return next(e)
39    limit = found
40    return ordering ? next({ ...e, context: [...(e.context ?? []), orderText(found)] }) : next(e)
41  }).catch(($, e, next) => next(e))
42
43  // A write of a prose file, once a limit is set: its words, and by section when over. The tool's result is unchanged.
44  for (const tool of WRITES) {
45    on('tool.call', { tool }, async ($, e, next) => {
46      const r = (await next(e)) as Result
47      const path = (e as unknown as { file_path?: unknown }).file_path
48      if (!gauging || r.deny !== undefined || r.isError || typeof path !== 'string') return r as never
49      const note = await gauge($, path).catch(() => undefined)
50      return (note === undefined ? r : { ...r, context: [...(r.context ?? []), note] }) as never
51    }).catch(($, e, next) => next(e))
52  }
53}
54
hooks/length.ts 110 lines
1// Writing to a length: the limit a prompt sets, a document's words as wc -w counts them, by section, and the two
2// texts the mod adds. Pure: no I/O.
3//
4// From the beyond-click bench (results-beyond.jsonl, 18 Oct): asked to revise a guide to at most 700 words, Claude
5// wrote the whole guide, counted 1,052 words, and wrote it again three times (798, 757, 694): each rewrite 2-7k
6// output tokens, and each copy stays in the conversation. Tasks without a limit wrote once.
7
8/** Files whose words are the point: prose, not code. */
9export const PROSE = /\.(md|mdx|markdown|txt|rst|adoc)$/i
10
11const NUMBER = String.raw`(\d{1,3}(?:[,.']\d{3})+|\d+)`
12// "at most 700 words", "under 250 words", "no more than 400 words", "max. 500 words", "to 800 words", "in 150 words",
13// "≤ 300 words"; German too: "höchstens 700 Wörter", "maximal 500 Wörter", "unter 300 Wörtern".
14const BEFORE = String.raw`(?:at\s+most|under|no\s+more\s+than|not\s+more\s+than|fewer\s+than|less\s+than|up\s+to|within|below|max(?:imum)?\.?(?:\s+of)?|limit(?:ed)?\s+(?:of|to)|to|in|≤|<=?|höchstens|maximal|max\.?|unter|bis\s+zu|nicht\s+mehr\s+als|auf)`
15const LIMIT = [
16  new RegExp(String.raw`\b${BEFORE}\s+(?:about\s+|around\s+|ca\.?\s+|etwa\s+)?${NUMBER}\s*(?:words?|wörtern?|worte)\b`, 'giu'),
17  new RegExp(String.raw`${NUMBER}\s*(?:words?|wörter)\s*(?:or\s+(?:fewer|less)|max(?:imum)?|at\s+most|oder\s+weniger|maximal|höchstens)\b`, 'giu'),
18  new RegExp(String.raw`\b${NUMBER}-words?\b`, 'giu'),
19]
20
21const toNumber = (s: string) => Number.parseInt(s.replace(/[,.']/g, ''), 10)
22
23/** The word limit a prompt sets, the last one it names; undefined when it sets none. */
24export function limitOf(text: string): number | undefined {
25  let found: { at: number; n: number } | undefined
26  for (const re of LIMIT) {
27    for (const m of text.matchAll(re)) {
28      const n = toNumber(m[1] as string)
29      if (n >= 20 && n <= 20_000 && (found === undefined || (m.index ?? 0) >= found.at)) found = { at: m.index ?? 0, n }
30    }
31  }
32  return found?.n
33}
34
35/** Words as wc -w counts them: runs of non-space. */
36export function wordsOf(text: string): number {
37  const m = text.match(/\S+/g)
38  return m === null ? 0 : m.length
39}
40
41export type Section = { title: string; words: number }
42
43/** The document's parts by its second-level headings (else third-level), with what comes before the first. */
44export function sectionsOf(text: string): Section[] {
45  const lines = text.split('\n')
46  let fence = false
47  const heads: { line: number; level: number; title: string }[] = []
48  lines.forEach((l, i) => {
49    if (/^\s*(```|~~~)/.test(l)) fence = !fence
50    const m = fence ? null : /^(#{2,3})\s+(.+?)\s*#*\s*$/.exec(l)
51    if (m) heads.push({ line: i, level: (m[1] as string).length, title: m[2] as string })
52  })
53  const level = heads.some(h => h.level === 2) ? 2 : 3
54  const cuts = heads.filter(h => h.level === level)
55  if (cuts.length === 0) return []
56  const out: Section[] = []
57  const head = lines.slice(0, (cuts[0] as { line: number }).line).join('\n')
58  if (wordsOf(head) > 0) out.push({ title: '(before the first section)', words: wordsOf(head) })
59  cuts.forEach((c, i) => {
60    const end = i + 1 < cuts.length ? (cuts[i + 1] as { line: number }).line : lines.length
61    out.push({ title: c.title, words: wordsOf(lines.slice(c.line, end).join('\n')) })
62  })
63  return out
64}
65
66const n = (x: number) => x.toLocaleString('en-US')
67const MAX_SECTIONS = 14
68const MAX_TITLE = 40
69
70/** Over by more than this share of the limit, one more full write at the target costs less than cutting with Edits:
71 * an Edit repeats the text it changes, and a cut spread over the document changes most of it. */
72export const REWRITE_OVER = 0.1
73/** A rewrite aims here, as a share of the limit: a writer's count of its own words runs long. */
74export const TARGET = 0.9
75
76const short = (title: string) => (title.length > MAX_TITLE ? `${title.slice(0, MAX_TITLE - 1)}…` : title)
77
78/** What a write of a prose file shows: its words against the limit; when over, what to do and each section's target. */
79export function gaugeText(path: string, text: string, limit: number): string {
80  const words = wordsOf(text)
81  if (words <= limit) return `${path}: ${n(words)} words (as wc -w counts), within the ${n(limit)} the prompt allows.`
82  const over = words - limit
83  const head = `${path}: ${n(words)} words (as wc -w counts), ${n(over)} over the ${n(limit)} the prompt allows.`
84  if (over <= limit * REWRITE_OVER) return `${head} A small overrun: trim it with one Edit, or a few in one step.`
85  const target = Math.floor(limit * TARGET)
86  const scale = target / words
87  const parts = sectionsOf(text)
88  const shown = parts.slice(0, MAX_SECTIONS).map(s => `${short(s.title)} ${n(s.words)} → ${n(Math.max(1, Math.round(s.words * scale)))}`)
89  const more = parts.length > MAX_SECTIONS ? ` · ${parts.length - MAX_SECTIONS} more` : ''
90  return (
91    `${head} Cuts this size change most of the text, and every Edit repeats what it changes: write it once more at about ${n(target)} words, every requested fact kept` +
92    (shown.length > 0 ? `, each section at its target: ${shown.join(' · ')}${more}.` : '.')
93  )
94}
95
96/**
97 * The person's setting beside a prompt that sets a word limit. In the person's name, where orders are followed; it
98 * says why, in what it costs.
99 */
100export function orderText(limit: number): string {
101  const aim = Math.round((limit * 0.85) / 10) * 10
102  return (
103    `The person's setting for writing to a length (${n(limit)} words here): first drafts run long, often by a third, and every write of the whole text ` +
104    `costs all of it again in output and stays in the conversation. So before writing, give each part its share of the words and aim for about ${n(aim)}, ` +
105    `keeping every fact the request asks for: cut wording, not facts. If those facts cannot all fit, keep them and say how far over the limit it runs, rather than dropping any. ` +
106    `Each write of the document then shows its words as wc -w counts them. If it is still well over, it shows each section's target: write it once more ` +
107    `to those, in one step, rather than cutting it down in rounds; trim a small overrun with one Edit.`
108  )
109}
110