@sleepy_panda_srl/portulan 0.3.0 → 0.4.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.
Files changed (90) hide show
  1. package/README.md +18 -6
  2. package/cli/README.md +56 -4
  3. package/cli/advisory.mjs +167 -53
  4. package/cli/cache-risk.mjs +69 -0
  5. package/cli/comments.mjs +8 -5
  6. package/cli/compile.mjs +492 -146
  7. package/cli/context.mjs +204 -43
  8. package/cli/doctor.mjs +211 -33
  9. package/cli/drills.mjs +32 -7
  10. package/cli/finish.md +34 -0
  11. package/cli/finish.mjs +9 -67
  12. package/cli/form.mjs +35 -19
  13. package/cli/frontmatter.mjs +39 -0
  14. package/cli/gate.mjs +14 -12
  15. package/cli/guidance-files.mjs +335 -0
  16. package/cli/handoff-policy.mjs +76 -0
  17. package/cli/html-entities.mjs +2130 -0
  18. package/cli/import-files.mjs +15 -0
  19. package/cli/index.mjs +77 -24
  20. package/cli/init-preview.mjs +553 -0
  21. package/cli/init.mjs +261 -46
  22. package/cli/instructions-journal.mjs +1185 -0
  23. package/cli/instructions.mjs +332 -57
  24. package/cli/ledger.mjs +123 -37
  25. package/cli/librarian.mjs +103 -19
  26. package/cli/persona-routing.mjs +209 -0
  27. package/cli/plugin-lint.mjs +77 -56
  28. package/cli/portulan.mjs +7 -2
  29. package/cli/recipe-output.mjs +91 -0
  30. package/cli/recipe-runner.md +68 -0
  31. package/cli/recipe-runner.mjs +131 -0
  32. package/cli/recipe-set.mjs +12 -3
  33. package/cli/ritual-contexts.mjs +103 -0
  34. package/cli/sessions.mjs +2 -1
  35. package/cli/spend-analysis.mjs +111 -0
  36. package/cli/spend-offer.md +53 -0
  37. package/cli/spend-offer.mjs +93 -0
  38. package/cli/stop-diagnostics.mjs +114 -0
  39. package/cli/stop-gate.mjs +133 -90
  40. package/cli/stop-policy.mjs +14 -0
  41. package/cli/symbols.mjs +55 -8
  42. package/cli/telemetry.mjs +19 -12
  43. package/cli/upgrade.mjs +62 -44
  44. package/cli/vendor.mjs +210 -126
  45. package/core/operating/context.md +368 -60
  46. package/core/operating/evolution.md +3 -2
  47. package/core/operating/loop.md +9 -4
  48. package/core/operating/sessions.md +6 -15
  49. package/core/personas/README.md +52 -10
  50. package/core/personas/implementer.md +3 -0
  51. package/core/personas/librarian.md +3 -0
  52. package/core/personas/reviewer.md +3 -0
  53. package/core/templates/handoff.md +6 -2
  54. package/evals/ab/personas/README.md +111 -0
  55. package/evals/ab/personas/acquisition.json +330 -0
  56. package/evals/ab/personas/bindings/implementer.txt +43 -0
  57. package/evals/ab/personas/bindings/librarian.txt +40 -0
  58. package/evals/ab/personas/bindings/reviewer.txt +41 -0
  59. package/evals/ab/personas/corpus.json +69 -0
  60. package/evals/ab/personas/grader.mjs +171 -0
  61. package/evals/ab/personas/implementer.json +774 -0
  62. package/evals/ab/personas/implementer.md +22 -0
  63. package/evals/ab/personas/librarian.json +758 -0
  64. package/evals/ab/personas/librarian.md +22 -0
  65. package/evals/ab/personas/protocol/continuation.mjs.txt +284 -0
  66. package/evals/ab/personas/protocol/first-outcome.mjs.txt +227 -0
  67. package/evals/ab/personas/protocol/first-probe.mjs.txt +172 -0
  68. package/evals/ab/personas/protocol/publisher.mjs.txt +126 -0
  69. package/evals/ab/personas/protocol/second-probe.mjs.txt +193 -0
  70. package/evals/ab/personas/repairs/first-control-answer.txt +23 -0
  71. package/evals/ab/personas/repairs/grader-before-decode-repair.mjs.txt +143 -0
  72. package/evals/ab/personas/reviewer.json +758 -0
  73. package/evals/ab/personas/reviewer.md +22 -0
  74. package/evals/releases/0.4.0.json +154 -0
  75. package/evals/releases/0.4.0.md +69 -0
  76. package/evals/releases/0.4.1.json +154 -0
  77. package/evals/releases/0.4.1.md +69 -0
  78. package/package.json +6 -2
  79. package/packs/rituals/checkpoints/README.md +35 -0
  80. package/packs/rituals/checkpoints/pack.json +3 -2
  81. package/packs/tools/local-llm/README.md +46 -6
  82. package/packs/tools/local-llm/runner/local-llm.mjs +127 -13
  83. package/packs/tools/local-llm/skills/delegate-local/SKILL.md +26 -2
  84. package/spec/README.md +37 -4
  85. package/spec/migrations/0009-instruction-sections-on-read.mjs +60 -11
  86. package/spec/migrations/0011-persona-bindings-compiled.mjs +23 -0
  87. package/spec/migrations/README.md +6 -0
  88. package/spec/pack.schema.json +12 -3
  89. package/spec/slots.md +262 -117
  90. package/spec/workspace.schema.json +67 -11
@@ -4,8 +4,9 @@
4
4
  > loads into every context is paid for again by every fresh context: each session, subagent, checkpoint
5
5
  > and persona. So guidance sits in load tiers, the always tier is budgeted like memory, and a demotion
6
6
  > is measured before and after. `loop.md` manages the window while a task runs; this page governs what
7
- > the host loads into it, and when. _(Proposal
8
- > [`0036`](../../.portulan/proposals/0036-what-a-host-loads-into-every-context-is-budgeted.md).)_
7
+ > the host loads into it, and when, then [what a change spends](#what-a-change-spends). _(Proposals
8
+ > [`0036`](../../.portulan/proposals/0036-what-a-host-loads-into-every-context-is-budgeted.md) and
9
+ > [`0038`](../../.portulan/proposals/0038-what-a-change-spends-is-measured.md).)_
9
10
 
10
11
  ## Four load tiers
11
12
 
@@ -48,11 +49,37 @@ raising the budget in the change that breached it. The shape is memory's on purp
48
49
  a budget is a rail, not an aim, and the no-raise half is a rule no checker establishes, for the reason
49
50
  [`memory.md`](memory.md) gives.
50
51
 
51
- **Bytes per token are declared too**, as `context.ratio`: measured exactly on demand, by asking the host
52
- for the true count, and recorded with the host whose exact count calibrated it. The exact measurement
53
- never runs inside a recipe, because no verify recipe may make a network call, and a ratio that moved
54
- between runs would make one tree red on one run and green on the next. The keys are argued in
55
- [`../../spec/slots.md`](../../spec/slots.md).
52
+ **Bytes per token are declared too**, as `context.ratio`, with the compile host whose measurement
53
+ domain the ratio describes. Calibration is deliberate, never repeated inside a recipe: no verify recipe
54
+ may make a network call, and a ratio that moved between runs would make one tree red on one run and green
55
+ on the next. The keys are argued in [`../../spec/slots.md`](../../spec/slots.md).
56
+
57
+ **On-demand provider count.** `portulan doctor --count-tokens MODEL_ID .portulan` sends the measured
58
+ Claude Code project always corpus to Anthropic using `ANTHROPIC_API_KEY` from the environment. Replace `MODEL_ID`
59
+ with a supported model; none is defaulted. The direct diagnostic also accepts
60
+ `node cli/context.mjs --workspace .portulan --count-tokens MODEL_ID`. Both send whole instructions,
61
+ imports and unscoped rules, but only extracted project skill, command and agent descriptions;
62
+ later-tier bodies and the measurement's personal/global/system/tool/plugin exclusions stay excluded.
63
+ Incomplete imports or invalid UTF-8 refuse before transport. Doctor validates the workspace and both
64
+ supported project-format budgets first; the request contains only the Claude Code corpus. Boot's
65
+ `--brief` line and the standalone context diagnostic also use that Claude Code domain. Count mode
66
+ summarizes validation failures without source values, and a separate `doctor --static` run
67
+ provides their details. The direct diagnostic validates its kind/tree/context domain, not the full schema.
68
+
69
+ The result is a **provider estimate**, including API framing and possibly system-added tokens;
70
+ actual host usage can differ. It reports provider/model, source bytes and an approximate effective
71
+ ratio, changes no manifest and does not override offline budget judgments. Assess the ratio manually
72
+ before declaring it for a compile host; `calibrated_by` remains that host's ID, never a provider/model
73
+ identifier. A ratio below the schema's minimum is reported as not declarable. An empty corpus makes
74
+ no request and supplies no ratio. Marius accepted “Provider count with stated limit” on 2026-11-09 for
75
+ [#506](https://github.com/sleepy-panda-srl/portulan/issues/506), qualifying proposal `0036`'s original
76
+ exact-host wording. The [provider guide](https://platform.claude.com/docs/en/build-with-claude/token-counting)
77
+ and [API contract](https://platform.claude.com/docs/en/api/messages/count_tokens) were checked that day.
78
+ This demonstrates a counting contract, not Claude Code's actual complete prompt.
79
+
80
+ Counting is unavailable with `--static` or in CI. The existing telemetry audit rejects literal
81
+ count-mode invocations in yielded recipe commands and their shell scripts; injected transports test
82
+ the contract offline. This is textual inspection of known modes, not arbitrary-shell prevention.
56
83
 
57
84
  **A report by default, a rail by declaration.** A workspace is failed only against a budget it declared.
58
85
 
@@ -84,7 +111,7 @@ Prefer moving content to a later tier over deleting it, keep one source per fact
84
111
  after. **A demotion states both figures.**
85
112
 
86
113
  **What this does not ask for**: a defaulted workspace rail, lossy compression, a codebase vector index,
87
- switching model or effort mid-session, output-shortening tricks, or any change to a host. Portulan
114
+ output-shortening tricks, or any change to a host. Portulan
88
115
  compiles to what hosts load and does not change how they load it.
89
116
 
90
117
  ## Every request pays for what the session has read
@@ -148,7 +175,331 @@ each section's anchor and size, the same way. It prints from the file as it is,
148
175
  go stale. Nothing checks that a session outlined first; what the rule saves is measured by rerunning a
149
176
  task, never by a recipe, which may not read the host's usage records.
150
177
 
151
- ## A local model runs without the host floor
178
+ ## What a handoff hands on
179
+
180
+ A handoff carries open state, decisions with their why, questions, the next action and what remains
181
+ unsafe. It links a fact's owning record instead of repeating the diff, measured figures or doctrine.
182
+ Its heading names the outcome; its filename already carries the date. Keep decisions and their why
183
+ when compressing: moving them and linking preserves them, dropping them does not.
184
+
185
+ A workspace may declare a per-handoff token cap and a cutoff at its explicit ratio. Only records dated
186
+ after that cutoff are bound; merged history is not rewritten. Compression or moving a fact to its
187
+ owning record repairs a breach, never splitting, retirement, a higher cap or a later cutoff in that
188
+ change. A correction counts toward the cap; if it would breach a merged record, put it in the correcting
189
+ session's handoff and name the corrected one. Review prevents backdating or moving a cutoff backward,
190
+ and advances it only with a tighter cap or lower ratio to exempt already merged history. The cutoff
191
+ day remains unbound because a date cannot separate morning merges from evening work.
192
+
193
+ The generated index may have its own token budget: a complete header and omission notice plus the
194
+ newest whole entry lines that fit. Omitted handoffs stay in the dated series, with no second index.
195
+ Both are on-read; neither belongs in the boot or always tier. The session-end gate checks existence
196
+ and a date, never length; the index recipe owns the upper cap. There is no aggregate series budget
197
+ and no minimum length. These are accepted proposal 0037's rules, implemented in Workspace Definition
198
+ 2.15; the schema contract and history-dependent limits are in [`spec/slots.md`](../../spec/slots.md).
199
+
200
+ ## What is machinery today, and what is not
201
+
202
+ As of Workspace Definition 2.9 a workspace can declare the budget and the ratio, and both are checked
203
+ for shape, by the schema and by `doctor`; the manifest key refuses a budget with no ratio to count it by.
204
+ **The always tier is measured and reported per project format.**
205
+ [`cli/context.mjs`](../../cli/context.mjs) measures Claude Code's project files and the root vendored
206
+ `AGENTS.md` in a workspace's `tree`. Doctor reports both without host-selection configuration. The
207
+ single declared cap is judged independently for each, never against their sum. A shared ratio is called
208
+ calibrated only where its declared calibration ID matches the reported format ID; a different or unknown
209
+ ID is explicitly extrapolated. The AGENTS measurement includes its
210
+ inline guidance and pointer text; pointer targets, at-sign expansion, personal, above-tree, nested
211
+ files and plugins are outside that domain. An absent optional root file is zero; a present root source
212
+ that is unreadable, nonregular, dangling or invalid UTF-8 is unmeasured, and an escaping AGENTS root
213
+ is unmeasured too. Without a budget that is a report; with one it cannot judge the declaration and
214
+ fails. Known excluded Claude imports and its ignored missing/depth-limited imports stay excluded.
215
+ Each host receives its verdict even when the other cannot be measured. These are project-file
216
+ estimates, not whole-host prompts. The boot closes with the Claude Code line. Split offers and priced
217
+ demotion levers describe that domain; the form report also recognizes a vendored AGENTS boot card and
218
+ requires its budget verdict to be resolved before applying an offered migration. Form reads check
219
+ canonical containment and regular-file status before opening project sources; its boot-unit source is
220
+ bounded to the workspace. Instruction-state advice checks ancestors too, while preserving valid local
221
+ links and supplied prospective readers. This repository's
222
+ `context` recipe holds the boot, description and owned prompt rails described below. **A boot can
223
+ read a card instead of the slots**: the `boot` unit of `slots.context` (Workspace Definition 2.10) is
224
+ compiled into every context, imports whole the files it needs, writes out the lead sentences of others,
225
+ and names the file behind each line, so the rest loads when a rule's path or the card sends a session to
226
+ it. **A team's own instruction file can be split the same way, section by section**: a line
227
+ `<!-- portulan: on-read -->` under a heading of `CLAUDE.md` or `.claude/CLAUDE.md` is the team's word that
228
+ the section may leave every context, and `portulan upgrade` moves it, byte for byte and proved to
229
+ reassemble, into an on-read unit whose index line names the heading and its size in whole KB. `doctor`, the
230
+ boot and `init` offer it where the file is over `0036`'s 8,000-token offer floor, and `doctor` and the boot
231
+ wherever a declared budget is breached, naming there the command that splits while `upgrade` will not run;
232
+ a section is never moved for its size alone. Claude Code 2.1.281 drops a comment standing on its own lines
233
+ before it loads an instruction file or rule (read in its program text on 2026-09-24), so the mark and the
234
+ marker left behind cost nothing. This repository's own card holds its boot under the 7,500-token line of
235
+ 2026-09-23, railed at today's figure plus 2% by the same recipe. A pointer's repository is not measured,
236
+ because a pointer declares no tree. **A persona can declare what its context costs**: `model`, `effort`,
237
+ `omitClaudeMd` and `cacheLifetime`, refused when malformed and never defaulted, which `compile` writes into
238
+ the hand-written binding of a persona a composed pack ships, and the core personas when compiling this
239
+ engine's own declared tree, and byte-compares under `--check`, printing
240
+ each field a host cannot express, with the persona and the host, as its degradation report for personas.
241
+ The compiler and doctor select `agents/` in a plugin tree and `.claude/agents/` in an ordinary
242
+ project, using the same bounded marker classification. Existing bindings compile even without a gate
243
+ policy or guidance; missing adopter bindings remain unbound. Nonregular and unreadable inputs are
244
+ not treated as absence. The two composed product roles have real plugin bindings. The committed
245
+ four-tier fixture includes an on-invoke skill. An on-path unit can select a literal nested
246
+ `CLAUDE.md` instead of scoped rules. Its mark and portable ownership registry distinguish repairable
247
+ drift from human replacement and allow owned target retirement. The body refuses imports that could
248
+ change meaning at its destination. From a project-root Claude session, the nested file is loaded on
249
+ subtree access; starting within that subtree can load it at startup. An unimported nested file is
250
+ outside project-root budget accounting, while an explicit root import is included. The unit contract
251
+ and limits are in [the slot specification](../../spec/slots.md#slotscontext--the-guidance-a-host-loads-each-unit-in-its-tier).
252
+ The compile recipe checks the committed exemplars and their companion rules
253
+ without writing. These are file/discovery contracts, not evidence of host enablement, field honoring
254
+ or runtime savings. The acquired [native-child comparisons](../../evals/ab/personas/README.md)
255
+ observe the three named core roles, matching inherited parent/child models and
256
+ effective high/medium child effort. They do not isolate every compiled field or
257
+ host precedence rule. The core declarations choose
258
+ `model: inherit`, `omitClaudeMd: false` and `cacheLifetime: 5m`, subject to the host precedence described in
259
+ [`../personas/README.md`](../personas/README.md). **The librarian reports project always-tier headroom**
260
+ against a declared budget and puts conditional demotion drafts in its standing report
261
+ ([#508](https://github.com/sleepy-panda-srl/portulan/issues/508)). It drafts only loaded,
262
+ matching, compiler-owned pure always units from the workspace, excluding boot
263
+ cards, at-sign text, imported sources, expansions, links and stale targets.
264
+ Compiler ownership needing regeneration is repaired before drafts are offered.
265
+ An ownership plan covering another tree or workspace directory is refused.
266
+ The human decides whether guidance can wait,
267
+ supplies its description, and recompiles and measures before/after when applying
268
+ the demotion. The pass changes no guidance, and its measurement keeps the same
269
+ Claude Code project domain and exclusions as `context`; other hosts and full-host
270
+ context remain outside it. **The tier matrix reports which tiers each host expresses.** The Stop-gate
271
+ now captures recipe failures while the command runs, so later success noise cannot displace an earlier
272
+ failure from the runner's tail. It keeps stdout/stderr line state separate, omits recognized passing,
273
+ skipped and expected-TODO reporter records, and relays recognized failures with
274
+ up to eight following indented detail lines per marker. An unrecognized diagnostic is explicitly
275
+ labeled as fallback; a nonzero exit with only passing lines says no failure diagnostic was identified.
276
+ The excerpt, executor diagnostics and clipping notices fit 4,096 bytes; the whole recipe problem fits
277
+ 6,144 bytes, including bounded identity and error text. Full output requires an explicit verification
278
+ run. These are diagnostic excerpts, not a proof that arbitrary output was classified correctly.
279
+ The repository's `context` recipe also rails registered skill bodies (manual-only included), core and
280
+ composed persona charters and actual plugin bindings, deterministic native hook message profiles, and
281
+ vendored AGENTS.md profiles. The byte limits, inventories, fixed-input accounting and finite-profile
282
+ limits live in [the verify README](../../.portulan/verify/README.md#owned-prompt-profiles).
283
+ These checks cover repository growth, not arbitrary runtime payload size or whole-host context.
284
+ Row 12 of [`../../docs/plan.md`](../../docs/plan.md) remains in progress; remaining obligations are held
285
+ by the human gate rather than described as enforcement.
286
+
287
+ _(Provenance: proposal `0036`'s sealed incident, in which an adopting repository's always-loaded
288
+ instruction files measured 130k tokens per fresh context, cache writes were most of the spend, and every
289
+ subagent and checkpoint paid it again, because nothing measured, budgeted or demoted what the host
290
+ loads. Host facts from the Claude Code documentation on costs, prompt caching and memory, checked
291
+ 2026-09-21.)_
292
+
293
+ ## What a change spends
294
+
295
+ This is proposal [`0038`](../../.portulan/proposals/0038-what-a-change-spends-is-measured.md)'s second
296
+ part: the cost of re-reading and rebuilding what the first part loads. The rules below govern spend;
297
+ [`sessions.md`](sessions.md) applies them to session startup, cache lifetimes and host switches.
298
+
299
+ ### Price a change from usage records
300
+
301
+ **A change is priced from the host's own usage records, numbers only**, attributed to its branch and
302
+ then its pull request: requests, tokens by class (uncached, written by lifetime, read and output), fresh
303
+ contexts opened, largest context, hit rate, cache rebuilds and their causes, and tokens per changed line.
304
+ Per-block duplicates count once. Where the host keeps totals of its own, the measurement states its
305
+ difference from them; unavailable records are not evidence of zero spend.
306
+
307
+ Token accounting uses usage fields, and local-worker accounting uses recognized receipt lines in
308
+ recorded Bash results; conversation text is not a pricing signal. The receipt-format limit and the
309
+ separate local figures belong to [the local-model rules](#a-local-model-runs-without-the-host-floor).
310
+ [`cli/ledger.mjs`](../../cli/ledger.mjs) reads locally, without a network call. A verify recipe never
311
+ opens host usage records: `doctor --static` validates without them, and ledger fixtures supply synthetic
312
+ records for verification.
313
+
314
+ Ordinary `doctor` reports the current branch and the latest five timed owned
315
+ parent sessions across branches and worktrees. A parent's latest valid main or
316
+ child request timestamp ranks it; session-ID order breaks ties. All owned requests
317
+ in a selected session contribute, including undated requests. Wholly undated
318
+ sessions remain in branch totals and are excluded from recency. Branch absence
319
+ does not hide recent-session evidence from other branches.
320
+
321
+ For valid complete observations, raw class totals remain exact integers. Missing
322
+ or invalid counters retain closed validity metadata through per-block deduplication;
323
+ affected totals are partial and class analysis remains unassessed. Missing fields
324
+ can be completed by another block; a provided invalid counter remains unassessed.
325
+ Cache-read and total cache-write counts are observations too: absence is unknown,
326
+ while an explicit zero is known. Without an aggregate, both explicitly observed
327
+ lifetime buckets may supply the write sum; one bucket alone may not. Duplicate
328
+ bucket maxima are combined before deriving that sum; a derived sum never becomes
329
+ an observed aggregate ceiling. This follows the [provider's write-total definition](https://platform.claude.com/docs/en/build-with-claude/prompt-caching),
330
+ checked 2026-11-11. Explicit partial live input remains unpriced; null or absent
331
+ live usage retains the transcript fallback. Output uncertainty remains separate
332
+ from valid input-only restart arithmetic.
333
+ Beside valid totals, a normalized class index
334
+ uses uncached input at 1×, declared or general read/write multipliers and output
335
+ at an explicitly assumed 5×. It assumes uniform input rates across models and is
336
+ an estimate in input-equivalents, not actual monetary dominance or a bill. The
337
+ output assumption follows proposal 0038 and the [Claude pricing table](https://platform.claude.com/docs/en/about-claude/pricing),
338
+ checked 2026-11-11. Writes with unstated lifetime span the minimum and maximum of
339
+ both write rates; differing endpoint leaders make dominance uncertain. Ties,
340
+ partial records and unsafe arithmetic are named.
341
+
342
+ | Dominant index class | Lever to review |
343
+ |---|---|
344
+ | Cache reads | Session length and tool output |
345
+ | Cache writes | Fresh contexts, TTL gaps and cache busts, with inferred rebuild causes |
346
+ | Output | Effort and model by recorded role, with A/B evidence before changing a route |
347
+ | Uncached input | Cache engagement, eligibility and uncached tails |
348
+
349
+ Role labels come only from recognized, optionally namespaced `attributionAgent`
350
+ metadata. Unknown roles, effort and model families remain unknown; UUIDs and
351
+ conversation text do not identify roles. Model/effort alternatives remain
352
+ unpriced without quality evidence. The existing biggest-lever comparison covers
353
+ quantified demotion/restart candidates only. With explicit horizon and resume
354
+ expense, the report compares continuation reads with fresh writes, resumed reads
355
+ and recovery expense. A later main request outside the branch's owned scope makes
356
+ that session's restart comparison unavailable. These are fixed-context
357
+ counterfactuals from caller-supplied estimates, not forecasts.
358
+ Invalid or missing input counters also suppress advisory comparisons, status and
359
+ Stop holds. Invalid fresh input stays unpriced until compaction; missing fields
360
+ can be completed by duplicate observations. Unusable latest input waits for a
361
+ valid next request or completed missing fields. Validity markers and numeric
362
+ input vectors persist in the incremental cache. Changed or contradictory tracked
363
+ observations replay deduplicated records through the last consumed newline,
364
+ including a retained historical crossing; the normal valid append path remains
365
+ incremental. Aggregate input sums are checked after deduplication and in live
366
+ status data. Warm calls cannot turn unknown inputs into known zeros. Recorded
367
+ output uncertainty does not invalidate otherwise usable input-only restart math.
368
+
369
+ ### Restart at a computed threshold
370
+
371
+ **A restart comparison states its assumptions before advising a handoff.** Its
372
+ break-even context is `C* = F + (F × m_w + R) / (n × m_r)`: fresh context `F`,
373
+ write/read multipliers `m_w` and `m_r`, requests remaining `n`, and additional
374
+ resume expense `R` in uncached-input-token equivalents. The workspace declares
375
+ these in `spend`; undeclared multipliers and horizon use the stated general
376
+ figures. The horizon does not decrement automatically.
377
+
378
+ `spend.resume.tokens` includes handoff output, boot/state recovery and additional
379
+ rereads over that horizon, excluding the formula's existing fresh-context write
380
+ and baseline reads. These are weighted cost units: 2,000 output tokens at 5× cost
381
+ contribute 10,000 equivalents. The estimate is supplied, not predicted. An absent
382
+ estimate is unknown, distinct from an explicit zero. Its threshold is then only
383
+ a lower bound, and the report says to finish bounded work when that costs less.
384
+
385
+ [`cli/advisory.mjs`](../../cli/advisory.mjs) speaks once with a tool result or at
386
+ a prompt, again after a compaction, and shows the same assumptions on the status
387
+ line. A declared `restart: "block"` holds once only with an explicit horizon and
388
+ resume expense and finite, strictly positive estimated savings
389
+ `n × m_r × (C − F) − m_w × F − R`. Equality never holds. Existing block declarations
390
+ without both estimates become conditional reports without a hold. Nothing ends
391
+ the session automatically. [`spec/slots.md`](../../spec/slots.md#spend--the-figures-the-restart-threshold-is-computed-at)
392
+ gives the declaration contract. This refinement follows the bounded-run evidence
393
+ in [#503](https://github.com/sleepy-panda-srl/portulan/issues/503); proposal 0038 is
394
+ preserved as its dated decision record.
395
+
396
+ A delayed transcript batch retains its first usable crossing in the current
397
+ compaction generation, including that request's context, fresh context and price
398
+ assumptions. If later counts fall below threshold, the line identifies that
399
+ historical crossing and says the latest estimate does not favor restarting.
400
+ Status remains current; Stop uses only the latest comparison. Changed declared
401
+ prices, horizon or resume expense replay the transcript before selecting a
402
+ crossing under those assumptions. A controlled Claude Code trace and delayed
403
+ batch checks live in [`cli/fixtures/advisory-crossing/`](../../cli/fixtures/advisory-crossing/);
404
+ they do not promise a universal host transcript latency bound.
405
+
406
+ Message IDs retain their first-seen order and parent/child ownership across the
407
+ transcript. Repeated observations merge counters and validity into that request;
408
+ only a new distinct parent request consumes a pending parent compaction. The
409
+ public ledger figures and hook replay use the canonical request fold. Hooks keep
410
+ 16 hot identities and a fixed membership bitmap. Possible older IDs, including
411
+ bitmap collisions, trigger exact completed-prefix replay before a decision.
412
+ Saturation can increase replay work and transient memory with transcript size;
413
+ the bitmap bounds retained identity metadata, not total replay work.
414
+
415
+ Full compilation emits Claude prompt/tool restart advice and a status line for every authored
416
+ repository, demo or portfolio workspace, independently of a gate policy. Declared session switches
417
+ and ritual caps compose into that settings artifact; only explicit `spend.restart: "block"` adds
418
+ advisory Stop. Permissions and verification Stop still require a gate policy. Other adapters do not
419
+ compile this advice. A missing manifest or pointer does not adopt native settings.
420
+
421
+
422
+ ### Keep the prefix stable
423
+
424
+ `doctor` reports potential Claude Code cache risks from the declared tree's
425
+ `.claude/settings.json`, `.claude/settings.local.json` and `.mcp.json`. It checks
426
+ `opusplan` aliases and conditional Haiku plan upgrades, explicit project
427
+ `ENABLE_TOOL_SEARCH=false`, and `ToolSearch` permission denial. Local scalar/env
428
+ values override shared values; rejection and denial lists accumulate. MCP counts
429
+ distinguish eligible configured candidates from project-file approvals. Neither count
430
+ establishes trust, live activation or connectivity. User, managed, CLI and broader
431
+ model/MCP policies, the full MCP schema, experimental-beta disabling and other
432
+ selectors remain unassessed. Every assessment names its selectors and these
433
+ limits, including when no recognized risk is found. Unreadable or invalid project sources produce sanitized notes rather
434
+ than a cache-safe assurance. These reports change no validation verdict or settings.
435
+
436
+ The inspected behavior follows Claude Code's [model configuration](https://code.claude.com/docs/en/model-config#opusplan-model-setting),
437
+ [tool search](https://code.claude.com/docs/en/mcp#configure-tool-search) and
438
+ [project MCP rejection](https://code.claude.com/docs/en/settings-reference#disabledmcpjsonservers)
439
+ references, checked 2026-11-11. A detected setting is a potential risk, not a measured
440
+ cache miss or bill.
441
+
442
+ **Model and effort are chosen when a context is created**, and tool definitions are deferred wherever
443
+ the host can defer them. Content that varies per session — a git snapshot, a date, a fresh working
444
+ directory, a session ID — stays out of Portulan's own always tier and hook-injected text. It may leave
445
+ one session's cache intact while forfeiting the prefix that sessions in one directory share.
446
+
447
+ Portulan's repository makes changes to these static carriers reviewable through
448
+ its `cache-stability` recipe: loaded files, hook sources and commands, and gate
449
+ explanation inputs are compared with a committed full-content golden. Its lexical
450
+ inventory identifies suspicious literal forms; unchanged digests establish only
451
+ unchanged repository text. Runtime session values, arbitrary helper outputs and
452
+ the host's prefix remain outside that capture. The repository's
453
+ [recipe instructions](../../.portulan/verify/README.md#the-recipes) name the explicit
454
+ refresh command and the forced-red drill. This is its own rail, not an adopter's
455
+ automatically installed policy.
456
+
457
+ A hosted session whose system prompt names the session itself starts cold whatever a workspace compiles;
458
+ that is inferred from such a prompt, not documented. The [session-start rules](sessions.md#a-session-starts-warm-or-it-pays-the-prefix-again)
459
+ describe the host's warm-start conditions; a workspace cannot repair the host's prefix by compiling one
460
+ of its own.
461
+
462
+ ### Routing is declared and earned
463
+
464
+ **The spawner states effort and how many contexts it may open, alongside what it loads**, extending
465
+ [the fresh-context declaration](#a-fresh-context-is-priced-where-it-is-created). Ritual packs can declare
466
+ `contributes.contexts.max_concurrent_subagents` under Pack Definition 1.1. Counts
467
+ compose by minimum and compile to Claude's project concurrency env setting even
468
+ without a gate policy. This controls ordinary fresh native Agent admission per
469
+ session, excluding the parent; resumes/forks, independent agent systems, host
470
+ overrides and experimental paths qualify it. It is no lifetime context, request
471
+ or token budget. Other adapters report it as not compiled, and the ledger counts
472
+ actual contexts either way. See [the pack contract](../../packs/rituals/checkpoints/README.md#context-admission)
473
+ and [the dated native trace](../../cli/fixtures/ritual-cap/README.md).
474
+
475
+ **A cheaper model or a lower effort for a role is earned by an A/B run before it is shipped.** A host
476
+ switch that changes the prefix meets the same evidence gate. The comparison must show reduced spend
477
+ without lost quality, and the shipped routing must be the routing its record supports. A cheaper
478
+ context that cannot do its role costs rework; a reviewer keeps the conventions it grades against.
479
+ Session switches retain their [specific guards](sessions.md#a-switch-that-changes-the-prefix-is-earned),
480
+ and local routing retains its screen-versus-comparison distinction below.
481
+ The three [core-persona records](../../evals/ab/personas/README.md) compare shipped
482
+ inheritance with a native medium-effort override on one concrete model. Both arms
483
+ pass the nine frozen cases; the shipped policy retains inheritance and undeclared
484
+ effort. Other parent models remain unmeasured. Plugin updates carry the baked
485
+ bindings; `upgrade` refreshes existing eligible bindings through the compiler's
486
+ ownership planner without creating foreign core adapters or changing settings.
487
+
488
+ ### Budget what returns to the model
489
+
490
+ **What a tool, recipe or hook hands back to the model is budgeted: failures in full, passes as a count,
491
+ and nothing echoed on every turn.** This is [the loop's compact-error rule](loop.md#context-is-the-budget)
492
+ applied to every output the model reads, with failure evidence retained so the saving cannot lower
493
+ quality. The reading rules above govern how a session asks for bounded output.
494
+
495
+ `doctor --agent` returns one aggregate count line on green. `recipe-set --agent`
496
+ wraps every yielded workspace and pack command in a shared executor: one counted
497
+ green line, complete stdout and stderr on failure. Direct commands retain human
498
+ output. Required tests and the existing CI job exercise that interface. Prefix
499
+ stability is a separate claim: a text baseline cannot establish runtime cache
500
+ behavior. The remaining demonstrations stay in [row 13](../../docs/plan.md#milestones).
501
+
502
+ ### A local model runs without the host floor
152
503
 
153
504
  A model served on the machine itself costs nothing per token, so `0038`'s dollar arithmetic gives it no
154
505
  restart threshold. What it does cost is time to prefill, memory for each context it holds, and recall that
@@ -175,16 +526,16 @@ each request after. So a local model is called as a context firewall, and four r
175
526
  receipt is unverified. _Why not atomic: a scratch worktree would judge the last commit plus the edit
176
527
  rather than the working copy plus the edit, would lack the untracked files a verify reads, and moving
177
528
  the edit back after exit `0` would not be atomic either (`0039` ruling 4)._
178
- 4. **Loopback is a rail, and routing is earned.** A local runner refuses an endpoint outside loopback
529
+ 4. **Loopback is a rail, and routing follows the [declared-and-earned gate](#routing-is-declared-and-earned).**
530
+ A local runner refuses an endpoint outside loopback
179
531
  (`127.0.0.1`, `localhost`, `[::1]`) unless leaving it is proposed and approved. A screen grades an
180
532
  installed model once per task against Portulan's own predicates, from
181
533
  [`cli/comments.mjs`](../../cli/comments.mjs), [`cli/doctor.mjs`](../../cli/doctor.mjs) and
182
534
  [`cli/skill-goldens.mjs`](../../cli/skill-goldens.mjs), and a model that fails it is not routed. Passing
183
- it earns nothing. A role is routed to a local model by an A/B record under `0038` rule 4: the local model
184
- as routed against the host's model on the same tasks, `k` runs per cell, captured once and railed to its
185
- register in `evals/ab/`. _Why: an endpoint off the machine sends workspace context away under a local
186
- tool's name, and the screen takes one sample per task with no baseline arm, while a cheaper model for a
187
- role is earned by an A/B run, never assumed (`0039` ruling 5)._
535
+ it earns nothing. For the local comparison, use the local model as routed against the host's model on the
536
+ same tasks, `k` runs per cell, captured once and railed to its register in `evals/ab/`. _Why: an endpoint off the machine sends workspace context away under a local
537
+ tool's name, and the screen takes one sample per task with no baseline arm, so a screen is not routing
538
+ evidence (`0039` ruling 5)._
188
539
 
189
540
  **What the runner holds today.** The [`tools/local-llm`](../../packs/tools/local-llm/README.md) pack's
190
541
  runner holds rules 1 to 3 and rule 4's loopback rail for the calls it makes. It sends the model its own
@@ -199,6 +550,8 @@ rollback on a red or no-verdict verify offline, with no network call; the rollba
199
550
  the runner's tests. This repository composes the pack and registers its skill; the ledger reports local
200
551
  receipts beside the host's tokens. `doctor` includes that branch-owned usage report by default,
201
552
  with unavailable data reported as unmeasured; `--static` omits usage-record discovery for verification.
553
+ Receipts are recognized by their whole-line format in recorded Bash results: a matching echo can be
554
+ counted, and output omitted from the transcript is missed.
202
555
  The [captured A/B](../../evals/ab/local-worker.md) supports code
203
556
  at low effort (L5/H5), while shared prose is disabled because codify scored L0/H3, despite handoff's
204
557
  L5/H5. Operational defaults and explicit roles require the measured model at low effort; explicit
@@ -211,48 +564,3 @@ _(Provenance: proposal
211
564
  [`0039`](../../.portulan/proposals/0039-a-local-model-runs-without-the-host-floor-and-under-a-thinking-cap.md)'s
212
565
  measurement of two local models against three of this repository's tasks on 2026-10-27, and its rulings 4
213
566
  and 5 of 2026-10-30.)_
214
-
215
- ## What is machinery today, and what is not
216
-
217
- As of Workspace Definition 2.9 a workspace can declare the budget and the ratio, and both are checked
218
- for shape, by the schema and by `doctor`; the manifest key refuses a budget with no ratio to count it by.
219
- **The always tier is measured and reported.** [`cli/context.mjs`](../../cli/context.mjs) measures what
220
- Claude Code loads into every context from a workspace's `tree`; `doctor` reports that for every workspace
221
- and fails it only over a declared budget or where a declared budget cannot be judged; the boot closes
222
- with the same line; and this repository's `context` recipe rails Portulan's own footprint. **A boot can
223
- read a card instead of the slots**: the `boot` unit of `slots.context` (Workspace Definition 2.10) is
224
- compiled into every context, imports whole the files it needs, writes out the lead sentences of others,
225
- and names the file behind each line, so the rest loads when a rule's path or the card sends a session to
226
- it. **A team's own instruction file can be split the same way, section by section**: a line
227
- `<!-- portulan: on-read -->` under a heading of `CLAUDE.md` or `.claude/CLAUDE.md` is the team's word that
228
- the section may leave every context, and `portulan upgrade` moves it, byte for byte and proved to
229
- reassemble, into an on-read unit whose index line names the heading and its size in whole KB. `doctor`, the
230
- boot and `init` offer it where the file is over `0036`'s 8,000-token offer floor, and `doctor` and the boot
231
- wherever a declared budget is breached, naming there the command that splits while `upgrade` will not run;
232
- a section is never moved for its size alone. Claude Code 2.1.281 drops a comment standing on its own lines
233
- before it loads an instruction file or rule (read in its program text on 2026-09-24), so the mark and the
234
- marker left behind cost nothing. This repository's own card holds its boot under the 7,500-token line of
235
- 2026-09-23, railed at today's figure plus 2% by the same recipe. A pointer's repository is not measured,
236
- because a pointer declares no tree. **A persona can declare what its context costs**: `model`, `effort`,
237
- `omitClaudeMd` and `cacheLifetime`, refused when malformed and never defaulted, which `compile` writes into
238
- the hand-written binding of a persona a composed pack ships and byte-compares under `--check`, printing
239
- each field a host cannot express, with the persona and the host, as its degradation report for personas.
240
- Claude Code 2.1.317 takes all four on an agent, and loads `agents/` only from a plugin root (both read
241
- in its program text on 2026-11-03), so in a tree that is not a plugin the compiled block is loaded by
242
- nothing, and the degradation report says so; in one that is, only while the plugin is installed and
243
- enabled, which `compile` cannot see. That stops short of the row: `compile` writes no
244
- `.claude/agents/`, where a repository that is not a plugin loads its agents, and no binding where none
245
- exists; a core persona's binding in this plugin's `agents/` is not compiled; and no host has been shown
246
- honouring a compiled field. **Still to land**: the
247
- exact mode that calibrates a ratio, the degradation report's half that says which tiers a host can
248
- express, the Stop-gate's named demotion and the librarian pass, one change at a time, in `0036`'s order
249
- of work and under row 12 of
250
- [`../../docs/plan.md`](../../docs/plan.md).
251
- Until each lands, the rule it would enforce is held by the human gate, and saying so is the point: a rule
252
- nothing checks, presented as one that something does, is the failure this page exists to prevent.
253
-
254
- _(Provenance: proposal `0036`'s sealed incident, in which an adopting repository's always-loaded
255
- instruction files measured 130k tokens per fresh context, cache writes were most of the spend, and every
256
- subagent and checkpoint paid it again, because nothing measured, budgeted or demoted what the host
257
- loads. Host facts from the Claude Code documentation on costs, prompt caching and memory, checked
258
- 2026-09-21.)_
@@ -68,8 +68,9 @@ moments**: the plan before building starts, the diff before it goes outward, and
68
68
  the close — where done is *demonstrated, not asserted* (`verification.md`). Two conditions hold the
69
69
  cycle up and neither is optional. The unit of work carries **hard exit criteria** fixed before it
70
70
  begins, so the close has a standard to grade rather than an impression. And **every change writes its
71
- record** (`loop.md`), its why in the commit and any open work in a handoff, so the context arriving at
72
- the next moment inherits the reasoning instead of re-deriving it — a graded moment whose grader must first reconstruct what was decided is
71
+ record** (`loop.md`), its why in the commit and any open work under the workspace's record policy, so
72
+ the context arriving at the next moment inherits the reasoning instead of re-deriving it — a graded
73
+ moment whose grader must first reconstruct what was decided is
73
74
  grading its own reconstruction.
74
75
 
75
76
  The altitudes stay separate, and the separation is the part that travels. Core states that the three
@@ -29,12 +29,17 @@ The loop runs inside a finite attention window, so context is managed, not hoard
29
29
  - **A change's why goes in its commit; a handoff carries only the work still open.** A commit's subject
30
30
  says what changed and its body says why, in a few lines: the reasoning the next agent cannot
31
31
  reconstruct from the diff, kept beside the diff. A session (one bounded working stretch, however the
32
- host names it) that ends with work not committed and pushed ends with a dated handoff: where things
33
- stand, the open questions and the single next action, and nothing the diff or a commit already says.
32
+ host names it) that leaves work not committed and pushed writes a dated handoff when its workspace
33
+ declares `slots.handoffs`: where things stand, the open questions and the single next action, and
34
+ nothing the diff or a commit already says.
34
35
  **Dated** means the filename leads with an ISO date, `YYYY-MM-DD-{slug}.md`, because a rule justified by
35
36
  being checkable has to say what the checker looks at, and the series then sorts chronologically for
36
- free. The session-end gate (milestone 4) checks exactly that: while the tree holds uncommitted or
37
- unpushed work, a handoff dated today exists, never its length or its shape. The librarian (milestone 5)
37
+ free. The session-end gate (milestone 4) checks that declared series: while the tree holds uncommitted
38
+ or unpushed work, a handoff dated today exists, never its length or its shape. A workspace may cap new handoffs in its verify recipe (see `context.md`); no minimum
39
+ length applies. With no series declared,
40
+ it asks for the work to be committed and pushed, its why in the commit message, and never asks for a
41
+ handoff. The workspace owns that record policy ([#505](https://github.com/sleepy-panda-srl/portulan/issues/505));
42
+ the verify recipe and the gate's retry bounds still apply. The librarian (milestone 5)
38
43
  reads the series: it indexes it, ages it from git, and mines it, since an incident nothing in the
39
44
  curated layer points back at is a candidate for the `codify` skill.
40
45
  _(Provenance: Cognition — handoffs record decisions and their rationale; Git's own SubmittingPatches —
@@ -5,9 +5,8 @@
5
5
  > On Claude Code that prefix comes before any workspace file, and cache writes were most of what a task
6
6
  > cost on this repository ([`../../evals/ab/warm.md`](../../evals/ab/warm.md) has the figures). So a task
7
7
  > runs in as few fresh contexts as it can, each starts warm where it can, the cache lifetime fits the gap
8
- > between uses, and a host switch that changes the prefix is earned by an A/B run.
9
- > [`context.md`](context.md) governs what the host loads; this page governs how sessions start and end
10
- > around it. _(Proposal [`0038`](../../.portulan/proposals/0038-what-a-change-spends-is-measured.md), rules
8
+ > between uses. [The spend rules](context.md#what-a-change-spends) govern pricing, restart, prefix stability,
9
+ > routing and output budgets; this page applies them to session startup, cache lifetimes and host switches. _(Proposal [`0038`](../../.portulan/proposals/0038-what-a-change-spends-is-measured.md), rules
11
10
  > 2 to 4, and row 12 of [`../../docs/plan.md`](../../docs/plan.md), whose compile targets carry the cache
12
11
  > lifetime.)_
13
12
 
@@ -20,18 +19,11 @@ prefix at the read price; a cold one writes it again. Sessions running side by s
20
19
  already share it. Sessions that follow one another share it only while nothing between them changed it,
21
20
  and on a local checkout the host's startup git snapshot changes with every commit.
22
21
 
23
- What moves the break earlier is anything per session in front of the task: a git snapshot, a date, a
24
- working directory made fresh for each run, a hook's per-session output, a session's own id. Rule 3 of
25
- `0038` keeps these out of Portulan's own always tier and hook text. A hosted session whose system prompt
26
- names the session itself starts cold whatever a workspace compiles; that is inferred from such a prompt,
27
- not documented.
22
+ The [prefix-stability rule](context.md#keep-the-prefix-stable) governs what may appear ahead of the task.
28
23
 
29
24
  ## Fewer fresh contexts per task
30
25
 
31
- - **Continue until the restart threshold, then restart.** `0038`'s threshold is computed from the multipliers,
32
- the fresh context and a horizon, which a workspace declares in `spend` (Workspace Definition 2.12) and which
33
- are the general ones where it does not, and [`../../cli/advisory.mjs`](../../cli/advisory.mjs) says it once.
34
- A restart that finds its prefix still cached costs less than the threshold assumes.
26
+ - **Restart policy:** follow [the computed threshold and handoff rule](context.md#restart-at-a-computed-threshold).
35
27
  - **Resume a finished worker within its cache lifetime** rather than start a new one: its context is
36
28
  read again rather than written again.
37
29
  - **A subagent is a fresh context**, on a five-minute cache unless set. Fan out when the reads it keeps
@@ -57,9 +49,8 @@ Claude Code has three switches that change what a session writes: the git instru
57
49
  snapshot together with the commit and pull-request instructions), the cache lifetime, and, for a run
58
50
  started headless, moving the per-machine sections (working directory, platform, memory paths, the
59
51
  snapshot) out of the system prompt and into the first message, so runs in different directories or on
60
- different machines share the system prompt. **Each stays off until an A/B run shows it cuts spend with no
61
- loss**, which is `0038` rule 4's gate applied to sessions, and **a session that needs what a switch
62
- removes keeps it**:
52
+ different machines share the system prompt. **Each follows the [declared-and-earned gate](context.md#routing-is-declared-and-earned)**,
53
+ and **a session that needs what a switch removes keeps it**:
63
54
 
64
55
  - **The git instructions are dropped only for sessions that neither commit nor open a pull request**, or
65
56
  where the workspace's own guidance carries its commit and pull-request workflow. A session that needs