@sleepy_panda_srl/portulan 0.1.3 → 0.3.0

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 (89) hide show
  1. package/README.md +7 -6
  2. package/cli/README.md +145 -358
  3. package/cli/advisory.mjs +354 -0
  4. package/cli/comments.mjs +932 -0
  5. package/cli/compile.mjs +1719 -1699
  6. package/cli/context.mjs +883 -0
  7. package/cli/control-chars.mjs +12 -301
  8. package/cli/discover.mjs +17 -574
  9. package/cli/doctor.mjs +269 -1080
  10. package/cli/drills.mjs +92 -504
  11. package/cli/feedback.mjs +16 -237
  12. package/cli/finish.mjs +437 -0
  13. package/cli/form.mjs +644 -0
  14. package/cli/fuzz-shell.mjs +20 -305
  15. package/cli/gate.mjs +7 -199
  16. package/cli/goldens.mjs +26 -184
  17. package/cli/index.mjs +305 -655
  18. package/cli/init.mjs +243 -566
  19. package/cli/inside.mjs +3 -28
  20. package/cli/instructions.mjs +829 -0
  21. package/cli/ledger.mjs +882 -0
  22. package/cli/librarian.mjs +135 -599
  23. package/cli/manifest.mjs +2 -39
  24. package/cli/mutants.mjs +9 -170
  25. package/cli/new.mjs +12 -243
  26. package/cli/pack-identity.mjs +5 -74
  27. package/cli/pack-version.mjs +13 -255
  28. package/cli/plugin-lint.mjs +42 -658
  29. package/cli/portulan.mjs +6 -125
  30. package/cli/recipe-set.mjs +15 -245
  31. package/cli/release-eval.mjs +28 -348
  32. package/cli/review-meter.mjs +29 -265
  33. package/cli/rule-carriers.mjs +12 -175
  34. package/cli/sessions.mjs +82 -0
  35. package/cli/skill-goldens.mjs +10 -176
  36. package/cli/skills-set.mjs +12 -414
  37. package/cli/stop-gate.mjs +83 -514
  38. package/cli/symbols.mjs +1132 -0
  39. package/cli/telemetry.mjs +82 -505
  40. package/cli/upgrade.mjs +171 -314
  41. package/cli/vendor.mjs +146 -476
  42. package/cli/version-carriers.mjs +8 -86
  43. package/core/README.md +1 -1
  44. package/core/engine.md +3 -9
  45. package/core/operating/autonomy.md +1 -1
  46. package/core/operating/context.md +258 -0
  47. package/core/operating/evolution.md +3 -3
  48. package/core/operating/loop.md +13 -20
  49. package/core/operating/memory.md +12 -3
  50. package/core/operating/sessions.md +100 -0
  51. package/core/personas/README.md +33 -0
  52. package/core/personas/librarian.md +2 -2
  53. package/core/skills/clarify/SKILL.md +1 -1
  54. package/core/skills/consolidate/SKILL.md +5 -3
  55. package/core/templates/handoff.md +28 -15
  56. package/core/templates/memory-entry.md +1 -0
  57. package/evals/README.md +271 -845
  58. package/evals/releases/0.2.0.json +146 -0
  59. package/evals/releases/0.2.0.md +67 -0
  60. package/evals/releases/0.3.0.json +150 -0
  61. package/evals/releases/0.3.0.md +68 -0
  62. package/evals/releases/README.md +3 -3
  63. package/package.json +5 -2
  64. package/packs/.claude-plugin/plugin.json +2 -1
  65. package/packs/README.md +7 -4
  66. package/packs/rituals/checkpoints/README.md +1 -1
  67. package/packs/tools/README.md +8 -8
  68. package/packs/tools/github/verify/actions-pinned.sh +10 -55
  69. package/packs/tools/local-llm/README.md +136 -0
  70. package/packs/tools/local-llm/pack.json +32 -0
  71. package/packs/tools/local-llm/personas/local-worker.md +44 -0
  72. package/packs/tools/local-llm/runner/local-llm.mjs +1665 -0
  73. package/packs/tools/local-llm/skills/delegate-local/SKILL.md +64 -0
  74. package/packs/tools/local-llm/verify/README.md +18 -0
  75. package/spec/README.md +113 -6
  76. package/spec/migrations/0001-repository-declares-its-tree.mjs +1 -26
  77. package/spec/migrations/0002-bundle-fallback-path.mjs +13 -74
  78. package/spec/migrations/0003-handoff-index-not-kept.mjs +66 -0
  79. package/spec/migrations/0004-changelog-fragments.mjs +53 -0
  80. package/spec/migrations/0005-session-log-retired.mjs +69 -0
  81. package/spec/migrations/0006-boot-card.mjs +77 -0
  82. package/spec/migrations/0007-guidance-compiled.mjs +31 -0
  83. package/spec/migrations/0008-card-reading.mjs +55 -0
  84. package/spec/migrations/0009-instruction-sections-on-read.mjs +60 -0
  85. package/spec/migrations/0010-comment-rail.mjs +57 -0
  86. package/spec/migrations/README.md +45 -10
  87. package/spec/pack.schema.json +1 -1
  88. package/spec/slots.md +343 -27
  89. package/spec/workspace.schema.json +145 -4
package/spec/slots.md CHANGED
@@ -50,8 +50,9 @@ experience a failure. *(Binding non-goal: no ceremony that can't scale down.)*
50
50
  | `slots.memory` | path | no | constitution — thesis 1 *(memory)*; [`memory.md`](../core/operating/memory.md) |
51
51
  | `slots.repos` | path | no | constitution — thesis 1 *(repo cards)*; [`repo-card.md`](../core/templates/repo-card.md) |
52
52
  | `slots.tasks` | path | no | content — [`tasks/`](../.portulan/tasks/); constitution — BMAD, the story file as atomic context unit |
53
- | `slots.handoffs` | path | no | content — [`handoffs/`](../.portulan/handoffs/); [`loop.md`](../core/operating/loop.md) — every session ends with one |
53
+ | `slots.handoffs` | path | no | content — [`handoffs/`](../.portulan/handoffs/); [`loop.md`](../core/operating/loop.md) — a session ending with work not committed and pushed ends with one |
54
54
  | `slots.proposals` | path | no | content — [`proposals/`](../.portulan/proposals/); [`evolution.md`](../core/operating/evolution.md) |
55
+ | `slots.context` | path | no | criterion — milestone 12, *`compile` emits Claude Code's on-path and on-invoke targets*; [`context.md`](../core/operating/context.md) — the four load tiers |
55
56
  | `verify` | structured | **for a governing kind** | criterion — *verify recipes*; [`verification.md`](../core/operating/verification.md) — the workspace sets the default |
56
57
  | `governed_by` | manifest | **for `kind: pointer`** | the maintainer's residence ruling, 2026-07-30 — [proposal 0017](../.portulan/proposals/0017-one-repository-one-governing-workspace.md); one repository, one governing workspace |
57
58
  | `products[]` | structured | no | criterion — *product-layer slot … portfolio-aware*; content — [`identity.md`](../.portulan/identity.md) |
@@ -60,6 +61,9 @@ experience a failure. *(Binding non-goal: no ceremony that can't scale down.)*
60
61
  | `packs` | structured | no | constitution — thesis 1's cascade, `core < pack < workspace` |
61
62
  | `memory` | structured | no | criterion — milestone 5, *generated size-budgeted index whose budget is a rail*; [`memory.md`](../core/operating/memory.md) — the Index and Consolidate states of the lifecycle |
62
63
  | `handoffs` | structured | no | criterion — milestone 5 as amended, *a generated index over the handoff series*; [`loop.md`](../core/operating/loop.md) — the librarian that mines the series |
64
+ | `context` | structured | no | criterion — milestone 12, *what a host loads into every context is measured, budgeted and demoted*; [`context.md`](../core/operating/context.md) — the always tier, budgeted in tokens |
65
+ | `sessions` | structured | no | criterion — milestone 13, *the prefix stays stable* and *earned by an A/B run*; [`sessions.md`](../core/operating/sessions.md) — which sessions start warm, and the switches that change the prefix |
66
+ | `spend` | structured | no | criterion — milestone 13, *the manifest declares the multipliers and the horizon*; [proposal 0038](../.portulan/proposals/0038-what-a-change-spends-is-measured.md), rulings 2 and 3 — the figures the restart threshold is computed at |
63
67
  | `provenance` | record field | **on every rule** | criterion — *provenance slot*; [proposal 0002](../.portulan/proposals/0002-sealed-provenance.md), adopted |
64
68
 
65
69
  ## `kind` — which of the four workspace kinds this is, and which of them govern
@@ -234,8 +238,8 @@ held then.
234
238
 
235
239
  `auto` · `propose` · `gated` · **`prohibited`**. The fourth is not a stronger `gated` — it is a different
236
240
  answer. Gated means *approvable per action* and compiles to a prompt; prohibited means *no approval
237
- exists* and compiles to a refusal. A three-class policy would file "no agent edits the constitution" under
238
- Gated, and the compiler would then emit a prompt — turning *never* into *unless someone clicks yes*.
241
+ exists* and compiles to a refusal. A three-class policy would file "no session certifies its own checkpoint"
242
+ under Gated, and the compiler would then emit a prompt — turning *never* into *unless someone clicks yes*.
239
243
 
240
244
  ### What a compiler must do with it
241
245
 
@@ -251,7 +255,7 @@ Two obligations, both learned from defects rather than designed in the abstract:
251
255
  ### `floor` — the platform floor this policy compiles to
252
256
 
253
257
  **Added in 2.2, optional**, inside the gate policy rather than the manifest — it is policy, and policy
254
- lives where the rules live. Four keys, and each one exists because it varies per repository and the
258
+ lives where the rules live. Its keys exist because it varies per repository and the
255
259
  floor backend would otherwise have to guess it:
256
260
 
257
261
  | Key | What it declares |
@@ -261,11 +265,15 @@ floor backend would otherwise have to guess it:
261
265
  | `reviews` | Required approving reviews. Declared rather than defaulted: 0 and 1 are a real difference, and a repository with one maintainer cannot use 1, because GitHub does not permit approving one's own pull request. |
262
266
  | `resolve_conversations` | Whether review threads must be resolved before merge. Omitting it would export a floor weaker than the one many repositories already run. |
263
267
 
264
- **What is deliberately NOT declarable is `strict`.** A pull request may not merge from behind its base
265
- ([proposal 0011](../.portulan/proposals/0011-no-merge-from-behind-main.md), applied live on
266
- 2026-07-27), so the export forces strict required status checks unconditionally. A policy able to
267
- declare `strict: false` would be a compiled artifact quietly undoing a ruling, in a diff nobody would
268
- read as one.
268
+ **Gate-policy 2.3 adds optional `strict`, a boolean defaulting to `true`.** It selects GitHub's native
269
+ strict-versus-loose required-status-check policy. Older gate-policy versions retain strict freshness
270
+ and refuse an explicit `strict` rather than silently ignoring it. A non-boolean is refused.
271
+
272
+ The earlier unconditional strict export encoded this repository's 2026-07-27 ruling as universal
273
+ mechanism. The maintainer's [2026-11-03 amendment](../.portulan/proposals/0040-allow-ten-commits-behind-main.md)
274
+ allows ten commits behind and blocks eleven here, which needs loose native freshness plus a separate
275
+ required check. The compiler emits the declared boolean; it neither implements that threshold for an
276
+ adopter nor applies the settings. Defaulting to `true` preserves every policy that omits the new key.
269
277
 
270
278
  A workspace with no `floor` compiles no floor: the backend refuses every rule *by name*, says the key
271
279
  is missing, and writes nothing. That is a legitimate shape — a workspace whose repositories are not on
@@ -323,9 +331,10 @@ at someone else's workspace.
323
331
 
324
332
  ## `products[]` — repeated, even though customer zero has one
325
333
 
326
- The criterion says *portfolio-aware: many products per workspace*, and the vision's Reference workspaces
327
- bullet describes a Sleepy Panda SRL workspace covering **all** Sleepy Panda SRL products. So the product layer is
328
- per-product from the start, not a workspace-level field that a later milestone has to unpick.
334
+ The criterion says *portfolio-aware: many products per workspace*, and
335
+ [`vision.md`](../docs/vision.md) § *Reference workspaces* gives the Sleepy Panda SRL workspace **all**
336
+ its products. So the product layer is per-product from the start, not a workspace-level field that a
337
+ later milestone has to unpick.
329
338
 
330
339
  This is the schema's most deliberate act of resisting its own evidence. Derived faithfully from the only
331
340
  available sample, `products` would be a singular object — customer zero has exactly one product, and a
@@ -350,9 +359,10 @@ claim it cannot check is whether the mission written there is still true.
350
359
 
351
360
  ## `affordances` — what the product offers an agent
352
361
 
353
- The unclaimed niche in the influence map: *agent-legibility, repo affordances scored by `doctor`*. The
354
- slot holds what an agent can rely on — entry points, contracts, budgets, commands — **and** what it must
355
- not assume, which is the half that makes the document worth reading. Customer zero's
362
+ The unclaimed niche [`vision.md`](../docs/vision.md) § *Agent-native / AX* names: agent legibility,
363
+ the repository's affordances scored by `doctor`. The slot holds what an agent can rely on — entry
364
+ points, contracts, budgets, commands — **and** what it must not assume, which is the half that makes
365
+ the document worth reading. Customer zero's
356
366
  [affordances document](../.portulan/products/portulan/affordances.md) is a filled example.
357
367
 
358
368
  It resolves down the cascade *inside* the workspace: a workspace-level `affordances` is the default, and
@@ -412,7 +422,7 @@ dependency between two keys needs `if`/`then` or `dependentRequired`, and neithe
412
422
  to anyone reading the schema alone. Stated loudly rather than hidden, because "read the schema to know
413
423
  the contract" is otherwise false in these places, and an unmarked exception is worse than a marked one.
414
424
 
415
- **It is now one of nine**, and [`README.md`](README.md) keeps the running count because the number is
425
+ **It is now one of ten**, and [`README.md`](README.md) keeps the running count because the number is
416
426
  the thing to watch. Two arrived with `memory` at 2.3 — a declared `memory` object needs a
417
427
  `slots.memory` store to index, and `memory.index.path` must resolve *outside* it — two more with
418
428
  `librarian` at 2.4, and then that same store-and-siting pair twice again: over the handoff series at 2.5
@@ -505,6 +515,7 @@ the store is the single source, committed so a change to what is always loaded i
505
515
  | `index.budget.columns` | The most columns one line may hold — refused, never truncated. |
506
516
  | `store.budget.kilobytes` | The most the store's records may total, in 1024-byte KB. |
507
517
  | `store.budget.record_kilobytes` | Added at 2.8. The most any **one** record may hold. |
518
+ | `store.budget.cutoff` | Added at 2.11, beside `record_kilobytes` only. The day after which that cap binds; forward only. |
508
519
 
509
520
  **Four numbers rather than one, because they are different axes — and each pairs with a per-unit cap
510
521
  that closes the hole the aggregate has.** The index is what gets loaded to decide what else to load,
@@ -526,6 +537,17 @@ workspace declares `record_kilobytes` alone, and [`../examples/workspace.json`](
526
537
  declares `kilobytes`, so both rails have a live carrier in the tree rather than one of them being a
527
538
  key the schema accepts and nothing exercises.
528
539
 
540
+ **The per-record cap can bind forward only, since 2.11**, in proposal `0037`'s shape for a handoff. With
541
+ `store.budget.cutoff` declared, the cap binds the records dated after the cutoff, and a record dated on or
542
+ before it is reported, never railed. A record's date is its `**dated:**` line, the day its text last
543
+ changed, because a verify rail may not read git; an edit re-dates a record, so an old one meets the cap
544
+ in the change that next rewrites it, and a cap lowered today does not turn a whole store red with no
545
+ change to the records. A missing or unreal date is refused, and so is one more than a day after the UTC
546
+ date the check runs on. The cutoff is the declaring change's date or the newest record's, whichever is
547
+ later; it moves only in a change that tightens the cap, and never earlier, which no checker
548
+ establishes (below). The cutoff requires
549
+ `record_kilobytes` beside it, one more conditional requirement the subset cannot write.
550
+
529
551
  **Nothing is defaulted**, on the `floor` object's rule from 2.2: a default here would be this
530
552
  specification setting a policy for every workspace that ever adopts it, in a key nobody typed. An
531
553
  undeclared number is not checked. A number that is declared and is not a positive integer is
@@ -548,13 +570,18 @@ not in the design; it was in the containment test.
548
570
  That rule is [`memory.md`](../core/operating/memory.md)'s and the human gate's — refusing a raise needs
549
571
  a check that reads git history, which produces false reds in a shallow CI checkout. The limit is
550
572
  recorded in [`../.portulan/verify/README.md`](../.portulan/verify/README.md) with the measurement
551
- behind it, rather than left for a reader to assume the rail covers both halves.
573
+ behind it, rather than left for a reader to assume the rail covers both halves. A `cutoff` moved
574
+ earlier, or later without a tighter cap, is the same hand edit, since it unbinds what the cap bound,
575
+ and the same gate holds it for the same reason.
552
576
 
553
577
  ## `handoffs` — an index over a series nobody can consolidate
554
578
 
555
579
  Added at 2.5, and it is deliberately one key. `slots.handoffs` is the series; this object says where
556
- its generated index goes. The same split as `memory` above, built by the same generator, byte-compared
557
- by the same recipe.
580
+ its generated index goes. The same split as `memory` above, built by the same generator. A workspace
581
+ keeps its index or not, and a copy on disk says which: a kept copy is byte-compared by the same recipe
582
+ and regenerated by a write; with none, `index --check` renders the series, which proves every handoff
583
+ yields a line, and nothing is written. This repository keeps none since 2026-09-23: a committed copy
584
+ conflicted on every merge that added a handoff.
558
585
 
559
586
  | Field | What it is |
560
587
  |---|---|
@@ -584,19 +611,16 @@ filename: a generated line whose title the generator invented is the one thing a
584
611
  not contain. The date is derived from the filename, which is where the cadence rule already puts it.
585
612
 
586
613
  **Its siting rule has two enforcers rather than one, and neither is redundant.** An index inside
587
- `slots.handoffs` is either counted as a handoff by `docs.sh`'s date-correspondence check — inflating
588
- one side of a count the Session log is held to — or, carrying no date, failed by the same check for
589
- exactly that. `index` refuses the siting outright, because it cannot render a series it would then be
614
+ `slots.handoffs` is either a dated handoff to `docs.sh`'s `record` check or, carrying no date, failed
615
+ by that check for exactly that. `index` refuses the siting outright, because it cannot render a series it would then be
590
616
  a member of. The second enforcer is not a second opinion: the generator physically cannot proceed,
591
617
  while the recipe is what catches a hand-placed file the generator never saw.
592
618
 
593
619
  **There is no budget here, and the absence is the argued half.** A budget's only permitted remedy is
594
620
  consolidation — merge, compress, split, retire ([`memory.md`](../core/operating/memory.md)) — and a
595
- handoff series is append-only by construction: one per session, dated, held to the Session log by a
596
- count-based correspondence. Retiring a handoff to buy headroom would either red that check or destroy
597
- the record it exists to keep; **splitting one** — the move 2.8 added for the per-record rail — would
598
- put two dated handoffs against a single session and red the same correspondence from the other side;
599
- and raising the budget in the change that breached it is the one repair that doctrine rules out. Every
621
+ handoff series is append-only by construction. Retiring a handoff to buy headroom destroys the record
622
+ it exists to keep; **splitting one** — the move 2.8 added for the per-record rail — leaves the series
623
+ no smaller; and raising the budget in the change that breached it is the one repair that doctrine rules out. Every
600
624
  remedy such a budget could ask for is barred, which makes it a rail designed to be broken — and a rail
601
625
  that fires with no legal repair is how a whole recipe gets switched off.
602
626
 
@@ -680,6 +704,298 @@ heading still holds. There is no budget for the same reason:
680
704
  there is nothing yet to budget, and the axis such a rail should use is per-persona rather than
681
705
  per-workspace, which belongs to the row where something finally reads these locations.
682
706
 
707
+ ## `context` — what the always tier may cost, and the ratio it is counted at
708
+
709
+ Added at **2.9**, with proposal `0036`. [`context.md`](../core/operating/context.md) is the doctrine and the
710
+ one source for what each load tier holds; this object is its machine half, as `memory` is
711
+ [`memory.md`](../core/operating/memory.md)'s.
712
+
713
+ | Field | What it is |
714
+ |---|---|
715
+ | `always.budget.tokens` | The most tokens the always tier may load, counted at the ratio below. |
716
+ | `ratio.bytes_per_token` | Bytes per token, as a host's exact count measured them. At least 1. |
717
+ | `ratio.calibrated_by` | The host whose exact count calibrated the ratio, by the id `compile` gives it: `claude-code`. |
718
+
719
+ ```json
720
+ "context": {
721
+ "always": { "budget": { "tokens": 8000 } },
722
+ "ratio": { "bytes_per_token": 2.99, "calibrated_by": "claude-code" }
723
+ }
724
+ ```
725
+
726
+ **The ratio is required and the budget is not.** A token budget with nothing to count it by is not a
727
+ budget, so the schema refuses `context` without `ratio`, and a ratio without the host that calibrated
728
+ it. Those are dependencies the subset can state, so they are the schema's and join no list of `doctor`'s
729
+ conditionals. A ratio alone is a legitimate shape: it is what a report in tokens needs, and the default
730
+ is the report.
731
+
732
+ **Declared, not measured per run.** The exact count asks the host, which is a network call no verify
733
+ recipe may make, and a ratio that moved between runs would make one tree red on one run and green on the
734
+ next. So the ratio is measured exactly on demand and declared, with the host that measured it. There is no
735
+ date field, because the commit that changes the ratio dates it, and one ratio rather than one per host,
736
+ because nothing yet reads a second.
737
+
738
+ **Named for its tier.** Only the always tier is budgeted, and the budget sits under `always` so a budget
739
+ on a later tier would sit beside it rather than force a rename, which would be a MAJOR.
740
+
741
+ **Nothing is defaulted**, on memory's rule: an undeclared budget is not checked. `doctor` refuses a
742
+ declared token budget that is not a positive integer, as it refuses the memory budgets, and a ratio under
743
+ 1, because a token covers at least one byte and a smaller figure is tokens per byte entered inverted.
744
+ Both are `doctor`'s because the subset has no `minimum` and cannot say `integer`. `doctor` also refuses
745
+ `context` in a manifest declaring a version before 2.9, whose own validator refuses it as unknown: the
746
+ key is gated from birth, when no manifest can newly fail the check.
747
+
748
+ **What checks the budget itself.** `doctor` measures the always tier of every workspace that declares
749
+ a `tree`, through [`../cli/context.mjs`](../cli/context.mjs), and fails it over a declared budget, or
750
+ where a declared budget cannot be judged; with none declared it reports the figure. _(This paragraph read
751
+ *"What nothing checks yet: the budget itself"* until that report landed, and it was true then:
752
+ [`a-mandate-nothing-checks-is-already-broken.md`](../.portulan/memory/a-mandate-nothing-checks-is-already-broken.md)
753
+ is why no workspace here declared the key before something checked it.)_ No workspace in this tree
754
+ declares it now either: Portulan's own contribution is railed by this repository's recipes, not by a
755
+ budget in its manifest. And, as with memory, no checker establishes that a budget was not raised in the
756
+ change that breached it.
757
+
758
+ ## `sessions` — the host switches that change what a fresh session writes
759
+
760
+ Added at **2.11**, from `0038`'s rules 3 and 4 and row 12's cache-lifetime compile target.
761
+ [`sessions.md`](../core/operating/sessions.md) is the doctrine; this object is its machine half.
762
+
763
+ | Field | What it is |
764
+ |---|---|
765
+ | `git_instructions` | `false` drops the host's git instructions from every session in the repository: on Claude Code, the startup git snapshot and the commit and pull-request instructions, compiled as `includeGitInstructions`. |
766
+ | `cache_lifetime` | `"5m"` or `"1h"`: the main conversation's cache lifetime, compiled as Claude Code's `promptCacheTtl`. |
767
+ | `headless.git_instructions` | The same switch, for the sessions Portulan's own runners start. |
768
+ | `headless.cache_lifetime` | The same lifetime, for those sessions. |
769
+ | `headless.exclude_dynamic_sections` | `true` starts those sessions with the per-machine sections moved from the system prompt into the first message, so runs in different directories or on different machines share the system prompt. |
770
+
771
+ ```json
772
+ "sessions": {
773
+ "headless": { "cache_lifetime": "5m", "exclude_dynamic_sections": true }
774
+ }
775
+ ```
776
+
777
+ **Nothing is defaulted.** An undeclared switch is the host's own default and `compile` emits nothing for
778
+ it, so a manifest without the key compiles byte for byte as before. A switch is declared once an A/B run
779
+ has shown that it cuts spend with no loss, and [`sessions.md`](../core/operating/sessions.md) says which
780
+ sessions keep what a switch removes.
781
+
782
+ **Two scopes, because the host has two.** `git_instructions` and `cache_lifetime` are project settings on
783
+ Claude Code, so `compile` writes them into the settings it already generates from `gates`, and they reach
784
+ every session there, interactive or not. The exclusion is no setting at all: Claude Code 2.1.281 takes it
785
+ only as a command-line flag or an SDK option, so only whatever starts a session can ask for it. `headless`
786
+ is therefore read by Portulan's own runners that start sessions, today [`../cli/warm.mjs`](../cli/warm.mjs),
787
+ which the package does not ship, and never compiled. A workspace with no gate policy has no settings
788
+ for the first two to ride, and `compile` says that it compiled neither.
789
+
790
+ **Offered to an adopter, and still never defaulted** (2026-09-24). `init` offers `cache_lifetime` `"5m"`
791
+ with what it saves and what a pause costs, asking once at a terminal or taking `--cache-lifetime`, and on
792
+ a yes drafts the key at 2.11 for `compile` to write; `upgrade` prints the same offer to a `repository`
793
+ workspace that declares no lifetime, and writes nothing; `doctor` reports where every switch stands in one
794
+ line. Only the lifetime is offered: the git instructions and the exclusion wait on their rows of
795
+ [`../evals/ab/warm.md`](../evals/ab/warm.md).
796
+
797
+ **Checked by the schema, and gated by `doctor`.** Every field is a boolean or one of two strings, which the
798
+ subset types in full, so nothing joins `doctor`'s hand-check. `doctor` refuses the key in a manifest
799
+ declaring a version before 2.11, gated from birth as `context` was.
800
+
801
+ ## `spend` — the figures the restart threshold is computed at
802
+
803
+ Added at **2.12**, from [proposal 0038](../.portulan/proposals/0038-what-a-change-spends-is-measured.md)'s
804
+ ruling 2, *the manifest declares the multipliers*, and its ruling 3's horizon; `restart` at **2.13**, from
805
+ the same ruling's block by declaration. The proposal carries the arithmetic; this object holds the figures a
806
+ workspace computes it at, and what a crossed threshold does.
807
+
808
+ | Field | What it is |
809
+ |---|---|
810
+ | `multipliers.read` | What a cache read costs, as a multiple of an uncached input token. Above 0 and at most 1. |
811
+ | `multipliers.write["5m"]` | What a five-minute cache write costs, as the same multiple. At least 1. |
812
+ | `multipliers.write["1h"]` | What a one-hour cache write costs, as the same multiple. At least 1. |
813
+ | `horizon.requests` | The requests still to go at which the restart threshold is judged. A positive integer. |
814
+ | `restart` | `"advise"` or `"block"`: what a crossed threshold does. Undeclared, `"advise"`. |
815
+
816
+ ```json
817
+ "spend": {
818
+ "multipliers": { "read": 0.05, "write": { "5m": 1.25, "1h": 2 } },
819
+ "horizon": { "requests": 30 },
820
+ "restart": "block"
821
+ }
822
+ ```
823
+
824
+ **What reads it.** A session is told to end into a handoff once its context passes
825
+ `C* ≈ F × (1 + m_w / (n × m_r))`: `F` the fresh context, `m_r` the read multiplier, `m_w` the write multiplier
826
+ at the lifetime the host recorded for the session's writes (the five-minute one where the records do not say),
827
+ and `n` the horizon. [`../cli/ledger.mjs`](../cli/ledger.mjs) reads the key from `<dir>/workspace.json` when
828
+ run with `--workspace <dir>`, and computes every threshold its report states at it. The restart advisory,
829
+ [`../cli/advisory.mjs`](../cli/advisory.mjs), is handed its command and the host's payload and nothing else,
830
+ so `compile` writes the figures onto each of its commands, the `PostToolUse` and `UserPromptSubmit` hooks, the
831
+ status line and, where `restart` declares it, the block, as `--read`, `--write-5m`, `--write-1h` and
832
+ `--horizon`, and its drift check holds them to the manifest: an edit to `spend` is drift until recompiled. A
833
+ workspace with no gate policy has no settings for them to ride, and `compile` says that it compiled nothing.
834
+ Every threshold the ledger or the advisory prints says whether its multipliers are declared, and so does a
835
+ ledger report with no threshold to judge.
836
+
837
+ **Declared, not defaulted.** Undeclared, both price at the general multipliers, a read at a tenth and writes
838
+ at 1.25× and 2×, over a horizon of 20 requests, and say `undeclared`. That is ruling 2's answer to a spread no
839
+ one figure fits: a model's cache reads cost between a fortieth and a tenth of an uncached input token. The
840
+ general tenth puts the restart line early, which is the cheaper way to err, since a restart that comes too
841
+ soon costs a fresh write and a handoff, and erring the other way, where reads cost a tenth, costs about four
842
+ times more in excess reads over 20 requests. A declaration makes a team's contracted rates and its host's real
843
+ prices one value. Either half may be declared alone, and the other keeps its general figures. The three
844
+ multipliers are one set, because a threshold priced by one declared figure and two general ones would be
845
+ priced by nobody's.
846
+
847
+ **`restart`: a block, only where declared.** `"advise"` is the advisory every workspace has: the line once,
848
+ with a tool result or at a prompt. `"block"` keeps that line and adds a block at the end of a turn: at the
849
+ first stop whose last recorded request is at or past the threshold and that no block provoked, the advisory's
850
+ own `stop` command, which `compile` writes as a second `Stop` command beside the Stop-gate's, holds the turn's
851
+ end with the line as its reason, once in a session and again after each compaction, and the turn goes on. A
852
+ stop that a block provoked is never held, since a host that gives that retry a new session id would find no
853
+ record of the hold and hold it again. It is the advisory's command rather than a reason inside the Stop-gate,
854
+ so the gate's counters and caps stay about the recipe and the handoff; and nothing ends the session, which
855
+ stays the agent's or the human's act. `0038`'s *Enforcement* says the block comes *instead* of the line, and
856
+ it is read here as *added*, because the line is what reaches an agent mid-stretch and a headless run's only
857
+ stop is its last (the coordinator session's delegated call of 2026-09-24); where the agent already acted on
858
+ the line, the block costs one request. Ruling 3 makes the block a workspace's own choice, so `init` asks
859
+ nothing about it and `upgrade` offers nothing: the threshold is an estimate, and one priced at the general
860
+ multipliers comes early for a model whose cache reads cost less than a tenth, where a block would hold a turn
861
+ short of its real line. Undeclared or `"advise"`, the settings compile byte for byte as before; where
862
+ `"block"` writes the command, `compile` says so on every run.
863
+
864
+ **What is not here.** Ruling 2 has `init` offer the declaration from the host's own pricing setting where the
865
+ host exposes one, else from a dated per-host table in the engine. Both are keyed by the model a host records,
866
+ so both wait for their own change: until then `init` offers no figures, and a workspace writes its own by
867
+ hand. Nothing checks a declared figure against what the workspace is billed; keeping it true is the declaring
868
+ workspace's work.
869
+
870
+ **Checked by the schema and by `doctor`, and refused by every reader.** The schema holds the shape: both
871
+ halves of `multipliers`, both lifetimes of `write`, and no key it does not name. The subset types the four
872
+ figures only as `number`, so `doctor` holds the read above 0 and at most 1, each write at least 1 and the
873
+ horizon to a positive integer by hand: a token read from cache costs something and never more than the same
874
+ token sent uncached, and writing one costs at least what sending it uncached does. Figures each in range can
875
+ still give no threshold, since it divides each write by the read, so `doctor` also refuses a write the read
876
+ divides past the largest number, which would put the restart line at Infinity, where no session reaches it.
877
+ The ledger, `compile` and the advisory hold the same ranges themselves rather than trusting that `doctor` ran.
878
+ The first two stop with exit 2; the advisory, whose exit 2 would erase the person's prompt, says once on
879
+ stderr what it could not use and prices that half at the general figures. A threshold that a session's fresh
880
+ context still carries past the largest number is none: the ledger stops with exit 2, and the advisory says it
881
+ is not known. `doctor` refuses the key in a manifest declaring a version before 2.12, gated from birth as
882
+ `sessions` was, and `restart` in one before 2.13. `compile` refuses `restart` there too, with exit 2 and
883
+ nothing written, so that no block is compiled from a declaration its own version's validator refuses.
884
+ `restart` is one of two strings, which the subset types in full, and the ledger and `compile` refuse any other
885
+ with exit 2.
886
+
887
+ ## `slots.context` — the guidance a host loads, each unit in its tier
888
+
889
+ Added at **2.10**, with proposal `0036`'s compile targets. [`context.md`](../core/operating/context.md)
890
+ defines the four tiers. This slot's pair is `context` above: the slot holds the guidance and the key its
891
+ budget, as `slots.memory` holds the records and `memory` their configuration.
892
+
893
+ A directory of Markdown files, one unit of guidance each; only the `.md` files at its top level are units,
894
+ and anything else in it is left alone. The file's name, less `.md`, is the unit's name and must be a slug,
895
+ because a host names a skill by it. Each file opens with frontmatter naming its tier:
896
+
897
+ ```markdown
898
+ ---
899
+ tier: on-path
900
+ paths: ["api/**", "migrations/*.sql"]
901
+ description: How this team writes an API handler and a migration.
902
+ ---
903
+
904
+ The guidance itself, which is what the host loads.
905
+ ```
906
+
907
+ | Key | What it is |
908
+ |---|---|
909
+ | `tier` | `always`, `on-path`, `on-invoke` or `on-read`: `0036`'s four tiers, spelled as that proposal spells them. |
910
+ | `paths` | On-path only, and required there: globs, relative to the repository the workspace governs, that load the unit when the agent first touches a match. A list, in flow form as above or as a block list of `- ` items. |
911
+ | `description` | Every tier but `always`, and one line: what an agent reads to decide whether to open the unit. It is a skill's description on a host that lists skills, and the pointer line wherever the unit degrades to one. |
912
+
913
+ **The tier is declared, never inferred**, and a unit sits in one tier, so what a unit costs is what its
914
+ tier costs and nothing has to read the file to find out where it belongs.
915
+
916
+ **`compile` emits it, per host.** For Claude Code, an `always` unit is an unscoped rule, loaded into every
917
+ context; an `on-path` unit is a rule scoped by `paths:`; an `on-invoke` unit is a project skill, whose
918
+ description is always loaded and whose body loads when it is invoked; and an `on-read` unit is one line in
919
+ an unscoped index of pointers, naming its file, its size in whole KB and its description (the size since
920
+ 2026-09-24, so a session knows what it passes over), while its file stays where it is for the agent to
921
+ open. **A tier a host cannot express degrades to an on-read pointer, never to nothing**: the vendored
922
+ `AGENTS.md` carries the `always` units inline and every other unit as a pointer, one level deep. The
923
+ compiled files are committed and byte-compared against this slot, as the compiled settings are against
924
+ `gates`, so a desktop session's fresh worktree has them. `compile` shows which rules in
925
+ `.claude/rules/portulan/` are its own with a marker there, `.compiled`, a file no host loads that lists
926
+ each rule it wrote: it rewrites or removes only a rule the marker lists, stops with exit 2 rather than
927
+ replace one it does not list, and stops too where the directory holds Markdown files and no marker in its
928
+ form. It writes and removes nothing through a link.
929
+
930
+ **Checked by `compile`, not by the schema or `doctor`.** The subset cannot see inside a file, so the
931
+ frontmatter is its consumer's to refuse: an unknown key, a tier outside the four, `paths` on a unit that is
932
+ not on-path or reaching outside the repository, a missing `description` or one on an `always` unit, a unit
933
+ with no guidance under its frontmatter, a link out of the workspace or into a directory `compile` writes into,
934
+ a slot in one of those (`.claude/`, or the workspace's `compile/`, neither of which `vendor` carries), a name
935
+ that is not a slug or is `on-read`, which the index takes, or a unit that would replace a skill written by
936
+ hand or compiled from another unit stops `compile` with exit 2, *could not compile*. `doctor` checks what it
937
+ checks of every directory slot, that the path resolves, and refuses the slot in a manifest declaring a version
938
+ before 2.10, gated from birth as `context` was.
939
+
940
+ **A unit named `boot` is the workspace's boot card**, reserved within 2.10, with no key and no version
941
+ bump (decided 2026-09-23). It is an `always` unit whose first line
942
+ under the frontmatter is `# Portulan boot card`, and the boot skill reads it in place of the slots when
943
+ that line is in its context. `compile` refuses a `boot` unit in another tier or opening with another line,
944
+ and a unit of another name opening with that one, which the skill would take for the card.
945
+
946
+ **A section of a project instruction file can become an on-read unit**, with no key and no version bump
947
+ (2026-09-24). A team marks it with a line `<!-- portulan: on-read -->` directly under its heading in
948
+ `CLAUDE.md` or `.claude/CLAUDE.md`, and `upgrade`'s form step `0009` moves the section, heading and all
949
+ and byte for byte, into a unit of this slot named by the heading, whose `description` is the heading's own
950
+ text, leaving `<!-- portulan: on-read <unit> <digest> -->` where it was, the digest the moved section's.
951
+ The file must reassemble from its units byte for byte, or nothing moves. Claude Code drops both comments
952
+ before it loads the file, so neither costs a context. The unit's text is not rewritten, so a relative link
953
+ in it still reads from the repository root, as it did in `CLAUDE.md`.
954
+ [`../cli/instructions.mjs`](../cli/instructions.mjs) is the same split on the command line, and `--join`
955
+ puts every section back as it stands, saying which one the digest shows edited since, and refusing a unit
956
+ re-tiered since, or a file not at the top of this slot, rather than undo its tier or remove it.
957
+
958
+ **An `always` unit may import whole files and write out another file's lead sentences or a gate policy's
959
+ gates**, so a card carries a fact without holding a second copy of it:
960
+
961
+ - **An import is `@` and a path, alone on its line, relative to the unit's own file.** Claude Code 2.1.281
962
+ loads a rule as it loads `CLAUDE.md`: it resolves an import against the file making it, follows it only
963
+ inside the project, skips code blocks, skips code spans and HTML comments except in a list item's text,
964
+ which it reads whole (2026-09-24), and loads nothing five imports below the rule. A rule scoped by
965
+ `paths:` loads on its path, and the files it imports load into every context, since an
966
+ imported file carries no `paths:` of its own (read in the program text, and seen on a fixture). So
967
+ `compile` spells each import again from the rule it compiles to, and refuses one that is a home or an
968
+ absolute path, leaves or links out of the tree it compiles, names no file, shares its line, or sits five
969
+ imports down; in a unit of any other tier it refuses an import naming a file, which would not load as
970
+ its unit does. `context.mjs` counts an imported file in the always tier at the depth the host loads it,
971
+ a path-scoped rule's included. The vendored `AGENTS.md` carries each import as a pointer line to the
972
+ file in the vendored tree, and one for each file that file imports in turn, to the depth the host would
973
+ load (2026-09-24), and `vendor` refuses an import leaving the workspace, which is all that tree holds.
974
+ - **A line `<!-- leads: <path> -->`, alone on its line, is replaced by the first sentence of each item in
975
+ the first list of the file it names**, each item opening with a bold lead. The file stays the one
976
+ source of those sentences, so a change there is drift until the unit is recompiled. `compile` refuses
977
+ a file with no list, an item with no bold lead, a lead carrying a link, which would not resolve from
978
+ the compiled rule, and a line of text with no indent straight under an item, which CommonMark reads as
979
+ more of that item (2026-09-24). **The path may end `#<heading>`**, for the first list under that
980
+ heading, found as `cli/symbols.mjs` finds a section: by the anchor GitHub gives it, which its outline
981
+ prints, or by its text or a `Parent>Child` path where either holds no space, since the line's path holds
982
+ none; a heading that none or several answer is refused (2026-09-24).
983
+ - **A line `<!-- engine: <path> -->`, alone on its line, writes out the leads of a file in the engine
984
+ `compile` ships with**, its path relative to that engine's `core/`, and refused outside it or naming no
985
+ file, with a `#<heading>` read as above (2026-09-24, within 2.10). Portulan's own card and each
986
+ consumer's name the same file, so they carry one text, and an upgrade of Portulan that changes it is
987
+ drift until the card is recompiled. `<plugin root>/` in a lead is the directory the plugin is installed
988
+ in, and is written as nothing where that engine is the tree compiled, as in Portulan's own repository.
989
+ - **A line `<!-- gates: <path> -->`, alone on its line, is replaced by the gate ids of the policy it
990
+ names, one line per tier** in core's order, each id as the policy spells it and in its order there, a
991
+ tier holding none saying so, and a line naming the packs the manifest composes, whose gates only a
992
+ compile resolving them can list (2026-09-24). The policy is read by the reader every compiled gate goes
993
+ through, so `compile` refuses here what it refuses there, and an edit to the policy is drift until the
994
+ unit is recompiled. `init` and `upgrade` draft a consumer's card with one.
995
+
996
+ **Nothing is defaulted.** A workspace that declares no slot compiles no guidance, and no directory in an
997
+ adopter's workspace is chosen by a key nobody typed.
998
+
683
999
  ## `provenance` — a record field, not a manifest key
684
1000
 
685
1001
  The criterion lists a provenance slot beside the others, but it does not belong in the manifest, and