@sleepy_panda_srl/portulan 0.1.3 → 0.2.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 (76) hide show
  1. package/README.md +6 -5
  2. package/cli/README.md +142 -358
  3. package/cli/advisory.mjs +354 -0
  4. package/cli/comments.mjs +932 -0
  5. package/cli/compile.mjs +1322 -1607
  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 +224 -1074
  10. package/cli/drills.mjs +68 -502
  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 +778 -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 +5 -124
  30. package/cli/recipe-set.mjs +15 -245
  31. package/cli/release-eval.mjs +28 -348
  32. package/cli/review-meter.mjs +17 -258
  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 +27 -492
  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 +183 -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/librarian.md +2 -2
  52. package/core/skills/consolidate/SKILL.md +4 -2
  53. package/core/templates/handoff.md +28 -15
  54. package/core/templates/memory-entry.md +1 -0
  55. package/evals/README.md +255 -845
  56. package/evals/releases/0.2.0.json +146 -0
  57. package/evals/releases/0.2.0.md +67 -0
  58. package/evals/releases/README.md +3 -3
  59. package/package.json +4 -2
  60. package/packs/rituals/checkpoints/README.md +1 -1
  61. package/packs/tools/github/verify/actions-pinned.sh +10 -55
  62. package/spec/README.md +112 -6
  63. package/spec/migrations/0001-repository-declares-its-tree.mjs +1 -26
  64. package/spec/migrations/0002-bundle-fallback-path.mjs +13 -74
  65. package/spec/migrations/0003-handoff-index-not-kept.mjs +66 -0
  66. package/spec/migrations/0004-changelog-fragments.mjs +53 -0
  67. package/spec/migrations/0005-session-log-retired.mjs +69 -0
  68. package/spec/migrations/0006-boot-card.mjs +77 -0
  69. package/spec/migrations/0007-guidance-compiled.mjs +31 -0
  70. package/spec/migrations/0008-card-reading.mjs +55 -0
  71. package/spec/migrations/0009-instruction-sections-on-read.mjs +60 -0
  72. package/spec/migrations/0010-comment-rail.mjs +57 -0
  73. package/spec/migrations/README.md +45 -10
  74. package/spec/pack.schema.json +1 -1
  75. package/spec/slots.md +333 -21
  76. package/spec/workspace.schema.json +145 -4
@@ -5,7 +5,7 @@
5
5
  > stated condition: a migration needed code. The runner is
6
6
  > [`../../cli/upgrade.mjs`](../../cli/upgrade.mjs).
7
7
 
8
- ## A step, and the two kinds
8
+ ## A step, and the three kinds
9
9
 
10
10
  A migration is not one act. It is an ordered set of **steps**, each a zero-dependency ESM module
11
11
  exporting a single `step` object, and each of exactly one kind:
@@ -14,6 +14,7 @@ exporting a single `step` object, and each of exactly one kind:
14
14
  |---|---|---|
15
15
  | `version` | A Workspace Definition MAJOR migration. `spec/README.md`'s rule stands: **MINORs are additive and owe no migration**, so there is no such thing as a MINOR step and nothing here restamps one. | `from` → `to` |
16
16
  | `repair` | Something a rewriter owes a workspace it touches, independent of any version — a value that was true where it was written and is not true where the workspace now is. | the workspace's own state |
17
+ | `form` | A move of a consumer's records or boot to the form Portulan moved its own to, in the repository beside the workspace as well as in it (2026-09-24). Today's form stays legitimate and boots as it did, so a form step is owed only where the new form is not there yet, and `doctor` reports the form without failing it. | the workspace's and its tree's own state |
17
18
 
18
19
  The second kind exists by the maintainer's ruling of 2026-08-12. Without it this directory would hold
19
20
  one step (`1.0 → 2.0`) with **no subject in this repository and none in any tree we have seen** —
@@ -22,19 +23,37 @@ machinery demonstrable only on a fixture. What actually bites an adopter is a wo
22
23
  ```js
23
24
  export const step = {
24
25
  id: "0002-bundle-fallback-path", // stable, sortable; the id IS the chain's order
25
- kind: "repair", // "version" | "repair"
26
- from: null, to: null, // version steps only; null on a repair
26
+ kind: "repair", // "version" | "repair" | "form"
27
+ from: null, to: null, // version steps only; null on a repair or a form step
27
28
  title: "…", // one line, printed in the plan
28
29
  why: "…", // why it is owed at all, printed under -v
29
- owed(ws), // → { owed: true | false | null, because }
30
+ owed(ws, ctx), // → { owed: true | false | null, because, hand? }
30
31
  plan(ws, ctx), // → { ok: true, edits } | { ok: false, reason }
31
32
  };
32
33
  ```
33
34
 
34
- `ws` is a read view of the workspace — `{ dir, manifest, manifestText, list(), read(rel) }`. `ctx`
35
- carries `{ bundle, spec, tree }`: the CLI root this run is executing from, the Workspace Definition
36
- version **this bundle implements**, and the `--tree` a step may need and must never invent. An `edit`
37
- is `{ file, next }` — a relative path and its whole next contents.
35
+ `ws` is a read view of the workspace — `{ dir, manifest, manifestText, list(), read(rel), repository }`,
36
+ `repository` being a read view of the tree the manifest declares, `{ dir, read(rel), names(rel), git }`,
37
+ or `null` where it declares none; `git` runs git in the tree, and is `null` where no work tree answers
38
+ there. `ctx` carries `{ bundle, spec, tree, today }`: the CLI root this run is executing from, the
39
+ Workspace Definition version **this bundle implements**, the `--tree` a step may need and must never
40
+ invent, and the date a record written today carries. An `edit` is `{ file, next }` — a relative path
41
+ and its whole next contents — relative to the workspace, or to the tree where it says `root: "tree"`;
42
+ a `next` of `null` deletes the file, and a `mode` is the mode of a file the edit creates, as a recipe
43
+ must be executable. Rollback puts back what each edit replaced or deleted, in either
44
+ root, and the report names each file as its root names it.
45
+
46
+ **After each step applied, the steps after it are asked again.** A step owed only once an earlier one
47
+ has landed, `0007` compiling the card `0006` drafts, is applied in the same run rather than the next.
48
+ **And after a pass that applied a step, the whole chain is asked again** until a pass applies nothing
49
+ (2026-09-24), so a later step can make an earlier one owed, as `0008` does `0007` by editing the card;
50
+ a chain still applying after as many passes as it has steps is refused and rolled back.
51
+ **A step owed where it does not place its edits answers `hand: true`** (2026-09-24), its `because` naming
52
+ what a person adds: `upgrade --write` applies the rest of the chain, reports that step as owed and not
53
+ placed once `doctor` is green, and exits 1, as `--check` does over a step owed.
54
+ A form step reads a workspace at this bundle's MAJOR only (`notYetForm` in
55
+ [`../../cli/form.mjs`](../../cli/form.mjs)): one a MAJOR behind is moved by a version step first, and
56
+ the form steps are then asked again, so a workspace no version step reaches is still refused.
38
57
 
39
58
  ## Three properties, and what each one replaces
40
59
 
@@ -69,7 +88,10 @@ is a question that could not be answered.
69
88
  claiming the migration caused it.
70
89
  - **Nothing here restamps a MINOR.** A manifest declares the version its content needs, not the newest
71
90
  one — `spec/README.md`'s own rule, the one that keeps `examples/` at 2.4 as live compatibility
72
- evidence and keeps `init`, `new` and `vendor` writing `2.7`.
91
+ evidence and keeps `new` and `vendor` writing `2.7`. `0006` writes 2.10 into a manifest older than it
92
+ because the step declares `slots.context`, the key 2.10 added, which is the same rule; `init`
93
+ writes 2.10 for the same reason, and 2.11 where a person takes the cache lifetime it offers, which
94
+ `sessions`, 2.11's key, declares.
73
95
 
74
96
  ## The steps that exist
75
97
 
@@ -77,11 +99,24 @@ is a question that could not be answered.
77
99
  |---|---|---|
78
100
  | [`0001-repository-declares-its-tree.mjs`](0001-repository-declares-its-tree.mjs) | `version` 1.0 → 2.0 | The one migration `spec/README.md` documents: a `repository` workspace must declare `tree`. It **will not guess** the value — `"../"` only where the parent is verifiably the repository root, otherwise it refuses and names `--tree`. |
79
101
  | [`0002-bundle-fallback-path.mjs`](0002-bundle-fallback-path.mjs) | `repair` | `init` bakes the bundle it ran from into `verify/index.sh` as an absolute path, marked `# portulan:bundle-fallback`. `vendor --switch` copies it byte for byte. The rail then exits **2 — could not run**, which is fail-closed and therefore easy never to notice. This re-derives it. |
102
+ | [`0003-handoff-index-not-kept.mjs`](0003-handoff-index-not-kept.mjs) | `form` | A kept handoff index is deleted and its path git-ignored, so none is committed again; `portulan index --handoffs` prints it. Owed by git's answer: a copy git ignores and does not track is one machine's own, and is left alone. |
103
+ | [`0004-changelog-fragments.mjs`](0004-changelog-fragments.mjs) | `form` | Each entry under `CHANGELOG.md`'s Unreleased becomes a fragment under `changes/`, and the heading keeps a two-line pointer. **Proved before it is offered**: the fragments, rendered as the cut renders them, must print each section back as the changelog held it, or the step refuses; a heading naming none of the six sections is refused too. The one change is an entry that does not open `- `, a `*` or `+` marker or a tab after one, whose fragment does since the cut accepts no other, and the step names each such entry by its line. |
104
+ | [`0005-session-log-retired.mjs`](0005-session-log-retired.mjs) | `form` | Each tracked Markdown file with a `Session log` holding entries keeps its heading and two lines naming the last commit that holds them. **Refused on a file with changes not committed**, since the pointer would name a commit that lacks them. |
105
+ | [`0006-boot-card.mjs`](0006-boot-card.mjs) | `form` | Where `slots.context` is undeclared, a boot card is drafted from the workspace's own files, as `init` drafts one, and the slot declared at 2.10. A declared slot with no `boot` unit is a workspace that boots through its slots, and is owed nothing. |
106
+ | [`0007-guidance-compiled.mjs`](0007-guidance-compiled.mjs) | `form` | The guidance half of `portulan compile` written where it drifted: the card and the rules the host loads, through `compile`'s own planner and refusals, and never `.claude/settings.json`, which enforces gates and is a person's to compile. |
107
+ | [`0008-card-reading.mjs`](0008-card-reading.mjs) | `form` | A card drafted before it carried the engine's rules on reading and the cache gets that section, which `compile` writes out from the installed Portulan's `core/operating/context.md`, and `0007` compiles it. A card lacking the section under any other head is owed it by hand: the run names the head the step looks for and the line to add, applies the rest of the chain and exits 1, and `doctor` names the card until the line is there. |
108
+ | [`0009-instruction-sections-on-read.mjs`](0009-instruction-sections-on-read.mjs) | `form` | Each section a team marks with a line `<!-- portulan: on-read -->` under its heading, in `CLAUDE.md` or `.claude/CLAUDE.md`, moves byte for byte into an on-read unit of `slots.context`, and a comment is left where it was. **Proved before it is offered**: the file must reassemble from its units byte for byte and each clause land once, or the step refuses; a section importing a file, a mark inside a marked section and a mark under no heading are refused too, and a mark in an instruction file that is a link is owed by hand while the rest of the chain runs. Owed only where a team marked a section, and put back by [`../../cli/instructions.mjs`](../../cli/instructions.mjs) `--join`. |
109
+ | [`0010-comment-rail.mjs`](0010-comment-rail.mjs) | `form` | Where a `repository` workspace's tree is one git lists and no `comments` recipe is declared, `verify/comments.sh` is drafted as `init` drafts it, holding the comment lines that record a change's history at the count the tree has, and declared. **Refused where `verify/comments.sh` is already there undeclared**, rather than written over. |
80
110
 
81
111
  **`0001` has no subject in this tree** — nothing declares 1.0 — and is exercised against a fixture,
82
112
  which is said here rather than dressed up. **`0002` has no subject in this repository either**: this
83
113
  workspace's `verify/index.sh` was written by hand, not drafted by `init`, and carries no marker. It is
84
- exercised against workspaces the real `init` drafts.
114
+ exercised against workspaces the real `init` drafts. **Neither have the form steps**: this repository
115
+ moved its own records and boot by hand on 2026-09-23 and 24, and `0003`–`0008` are exercised against a
116
+ consumer the real `init` drafts, put back in the form it drafted before, and committed. `0009` has none
117
+ either, since this repository keeps no `CLAUDE.md`, and is exercised against such a consumer whose
118
+ instruction file the suite marks; `0010` against one `init` drafted where git listed no files, then
119
+ committed.
85
120
 
86
121
  ## Adding one
87
122
 
@@ -132,7 +132,7 @@
132
132
  },
133
133
 
134
134
  "fragment": {
135
- "description": "One gate-policy fragment: an id-addressed rule this pack contributes, in the same shape .portulan/gates.json already uses, because that policy was deliberately modelled as a list of id-addressed rules with no dependence on being the only source so that a merge step would be an addition rather than a redesign (.portulan/compile/README.md). WHERE THE TIGHTEN-ONLY RULE IS ENFORCED, stated plainly because a schema implying a guarantee it does not carry is what .portulan/dod.md condition 4 refuses: tightening is relative to what the same id already holds in the layer beneath, and a manifest cannot see that layer. So the shape forecloses what shape can — `auto` is not in the tier enum below, which makes demotion-to-unattended unexpressible in a pack at all — and the remaining comparisons are the merge step's. TIGHTENING HAS TWO AXES AND THE MERGE CHECKS BOTH. The tier may only rise. The `action` may not change at all: a fragment naming an existing id must carry that rule's action unaltered, because raising the tier while replacing the matcher passes every tier comparison, is reported as a tightening, and removes the gate — measured, on a real policy, as a fragment that took `git push --force` from gated to prohibited and deleted the emitted gate in the same move. A pack that wants to gate a different action contributes a NEW id; changing what an existing rule matches belongs to the workspace, which owns its policy. Neither layer is sufficient alone, and a release that consumes fragments without the merge's refusals would be trusting what it claims to check.",
135
+ "description": "One gate-policy fragment: an id-addressed rule this pack contributes, in the same shape .portulan/gates.json already uses, because that policy was deliberately modelled as a list of id-addressed rules with no dependence on being the only source so that a merge step would be an addition rather than a redesign (.portulan/gate-map/compiler.md). WHERE THE TIGHTEN-ONLY RULE IS ENFORCED, stated plainly because a schema implying a guarantee it does not carry is what .portulan/dod.md condition 4 refuses: tightening is relative to what the same id already holds in the layer beneath, and a manifest cannot see that layer. So the shape forecloses what shape can — `auto` is not in the tier enum below, which makes demotion-to-unattended unexpressible in a pack at all — and the remaining comparisons are the merge step's. TIGHTENING HAS TWO AXES AND THE MERGE CHECKS BOTH. The tier may only rise. The `action` may not change at all: a fragment naming an existing id must carry that rule's action unaltered, because raising the tier while replacing the matcher passes every tier comparison, is reported as a tightening, and removes the gate — measured, on a real policy, as a fragment that took `git push --force` from gated to prohibited and deleted the emitted gate in the same move. A pack that wants to gate a different action contributes a NEW id; changing what an existing rule matches belongs to the workspace, which owns its policy. Neither layer is sufficient alone, and a release that consumes fragments without the merge's refusals would be trusting what it claims to check.",
136
136
  "type": "object",
137
137
  "additionalProperties": false,
138
138
  "required": ["id", "tier", "action", "reason"],
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
 
@@ -323,9 +327,10 @@ at someone else's workspace.
323
327
 
324
328
  ## `products[]` — repeated, even though customer zero has one
325
329
 
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.
330
+ The criterion says *portfolio-aware: many products per workspace*, and
331
+ [`vision.md`](../docs/vision.md) § *Reference workspaces* gives the Sleepy Panda SRL workspace **all**
332
+ its products. So the product layer is per-product from the start, not a workspace-level field that a
333
+ later milestone has to unpick.
329
334
 
330
335
  This is the schema's most deliberate act of resisting its own evidence. Derived faithfully from the only
331
336
  available sample, `products` would be a singular object — customer zero has exactly one product, and a
@@ -350,9 +355,10 @@ claim it cannot check is whether the mission written there is still true.
350
355
 
351
356
  ## `affordances` — what the product offers an agent
352
357
 
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
358
+ The unclaimed niche [`vision.md`](../docs/vision.md) § *Agent-native / AX* names: agent legibility,
359
+ the repository's affordances scored by `doctor`. The slot holds what an agent can rely on — entry
360
+ points, contracts, budgets, commands — **and** what it must not assume, which is the half that makes
361
+ the document worth reading. Customer zero's
356
362
  [affordances document](../.portulan/products/portulan/affordances.md) is a filled example.
357
363
 
358
364
  It resolves down the cascade *inside* the workspace: a workspace-level `affordances` is the default, and
@@ -412,7 +418,7 @@ dependency between two keys needs `if`/`then` or `dependentRequired`, and neithe
412
418
  to anyone reading the schema alone. Stated loudly rather than hidden, because "read the schema to know
413
419
  the contract" is otherwise false in these places, and an unmarked exception is worse than a marked one.
414
420
 
415
- **It is now one of nine**, and [`README.md`](README.md) keeps the running count because the number is
421
+ **It is now one of ten**, and [`README.md`](README.md) keeps the running count because the number is
416
422
  the thing to watch. Two arrived with `memory` at 2.3 — a declared `memory` object needs a
417
423
  `slots.memory` store to index, and `memory.index.path` must resolve *outside* it — two more with
418
424
  `librarian` at 2.4, and then that same store-and-siting pair twice again: over the handoff series at 2.5
@@ -505,6 +511,7 @@ the store is the single source, committed so a change to what is always loaded i
505
511
  | `index.budget.columns` | The most columns one line may hold — refused, never truncated. |
506
512
  | `store.budget.kilobytes` | The most the store's records may total, in 1024-byte KB. |
507
513
  | `store.budget.record_kilobytes` | Added at 2.8. The most any **one** record may hold. |
514
+ | `store.budget.cutoff` | Added at 2.11, beside `record_kilobytes` only. The day after which that cap binds; forward only. |
508
515
 
509
516
  **Four numbers rather than one, because they are different axes — and each pairs with a per-unit cap
510
517
  that closes the hole the aggregate has.** The index is what gets loaded to decide what else to load,
@@ -526,6 +533,17 @@ workspace declares `record_kilobytes` alone, and [`../examples/workspace.json`](
526
533
  declares `kilobytes`, so both rails have a live carrier in the tree rather than one of them being a
527
534
  key the schema accepts and nothing exercises.
528
535
 
536
+ **The per-record cap can bind forward only, since 2.11**, in proposal `0037`'s shape for a handoff. With
537
+ `store.budget.cutoff` declared, the cap binds the records dated after the cutoff, and a record dated on or
538
+ before it is reported, never railed. A record's date is its `**dated:**` line, the day its text last
539
+ changed, because a verify rail may not read git; an edit re-dates a record, so an old one meets the cap
540
+ in the change that next rewrites it, and a cap lowered today does not turn a whole store red with no
541
+ change to the records. A missing or unreal date is refused, and so is one more than a day after the UTC
542
+ date the check runs on. The cutoff is the declaring change's date or the newest record's, whichever is
543
+ later; it moves only in a change that tightens the cap, and never earlier, which no checker
544
+ establishes (below). The cutoff requires
545
+ `record_kilobytes` beside it, one more conditional requirement the subset cannot write.
546
+
529
547
  **Nothing is defaulted**, on the `floor` object's rule from 2.2: a default here would be this
530
548
  specification setting a policy for every workspace that ever adopts it, in a key nobody typed. An
531
549
  undeclared number is not checked. A number that is declared and is not a positive integer is
@@ -548,13 +566,18 @@ not in the design; it was in the containment test.
548
566
  That rule is [`memory.md`](../core/operating/memory.md)'s and the human gate's — refusing a raise needs
549
567
  a check that reads git history, which produces false reds in a shallow CI checkout. The limit is
550
568
  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.
569
+ behind it, rather than left for a reader to assume the rail covers both halves. A `cutoff` moved
570
+ earlier, or later without a tighter cap, is the same hand edit, since it unbinds what the cap bound,
571
+ and the same gate holds it for the same reason.
552
572
 
553
573
  ## `handoffs` — an index over a series nobody can consolidate
554
574
 
555
575
  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.
576
+ its generated index goes. The same split as `memory` above, built by the same generator. A workspace
577
+ keeps its index or not, and a copy on disk says which: a kept copy is byte-compared by the same recipe
578
+ and regenerated by a write; with none, `index --check` renders the series, which proves every handoff
579
+ yields a line, and nothing is written. This repository keeps none since 2026-09-23: a committed copy
580
+ conflicted on every merge that added a handoff.
558
581
 
559
582
  | Field | What it is |
560
583
  |---|---|
@@ -584,19 +607,16 @@ filename: a generated line whose title the generator invented is the one thing a
584
607
  not contain. The date is derived from the filename, which is where the cadence rule already puts it.
585
608
 
586
609
  **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
610
+ `slots.handoffs` is either a dated handoff to `docs.sh`'s `record` check or, carrying no date, failed
611
+ by that check for exactly that. `index` refuses the siting outright, because it cannot render a series it would then be
590
612
  a member of. The second enforcer is not a second opinion: the generator physically cannot proceed,
591
613
  while the recipe is what catches a hand-placed file the generator never saw.
592
614
 
593
615
  **There is no budget here, and the absence is the argued half.** A budget's only permitted remedy is
594
616
  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
617
+ handoff series is append-only by construction. Retiring a handoff to buy headroom destroys the record
618
+ it exists to keep; **splitting one** — the move 2.8 added for the per-record rail — leaves the series
619
+ no smaller; and raising the budget in the change that breached it is the one repair that doctrine rules out. Every
600
620
  remedy such a budget could ask for is barred, which makes it a rail designed to be broken — and a rail
601
621
  that fires with no legal repair is how a whole recipe gets switched off.
602
622
 
@@ -680,6 +700,298 @@ heading still holds. There is no budget for the same reason:
680
700
  there is nothing yet to budget, and the axis such a rail should use is per-persona rather than
681
701
  per-workspace, which belongs to the row where something finally reads these locations.
682
702
 
703
+ ## `context` — what the always tier may cost, and the ratio it is counted at
704
+
705
+ Added at **2.9**, with proposal `0036`. [`context.md`](../core/operating/context.md) is the doctrine and the
706
+ one source for what each load tier holds; this object is its machine half, as `memory` is
707
+ [`memory.md`](../core/operating/memory.md)'s.
708
+
709
+ | Field | What it is |
710
+ |---|---|
711
+ | `always.budget.tokens` | The most tokens the always tier may load, counted at the ratio below. |
712
+ | `ratio.bytes_per_token` | Bytes per token, as a host's exact count measured them. At least 1. |
713
+ | `ratio.calibrated_by` | The host whose exact count calibrated the ratio, by the id `compile` gives it: `claude-code`. |
714
+
715
+ ```json
716
+ "context": {
717
+ "always": { "budget": { "tokens": 8000 } },
718
+ "ratio": { "bytes_per_token": 2.99, "calibrated_by": "claude-code" }
719
+ }
720
+ ```
721
+
722
+ **The ratio is required and the budget is not.** A token budget with nothing to count it by is not a
723
+ budget, so the schema refuses `context` without `ratio`, and a ratio without the host that calibrated
724
+ it. Those are dependencies the subset can state, so they are the schema's and join no list of `doctor`'s
725
+ conditionals. A ratio alone is a legitimate shape: it is what a report in tokens needs, and the default
726
+ is the report.
727
+
728
+ **Declared, not measured per run.** The exact count asks the host, which is a network call no verify
729
+ recipe may make, and a ratio that moved between runs would make one tree red on one run and green on the
730
+ next. So the ratio is measured exactly on demand and declared, with the host that measured it. There is no
731
+ date field, because the commit that changes the ratio dates it, and one ratio rather than one per host,
732
+ because nothing yet reads a second.
733
+
734
+ **Named for its tier.** Only the always tier is budgeted, and the budget sits under `always` so a budget
735
+ on a later tier would sit beside it rather than force a rename, which would be a MAJOR.
736
+
737
+ **Nothing is defaulted**, on memory's rule: an undeclared budget is not checked. `doctor` refuses a
738
+ declared token budget that is not a positive integer, as it refuses the memory budgets, and a ratio under
739
+ 1, because a token covers at least one byte and a smaller figure is tokens per byte entered inverted.
740
+ Both are `doctor`'s because the subset has no `minimum` and cannot say `integer`. `doctor` also refuses
741
+ `context` in a manifest declaring a version before 2.9, whose own validator refuses it as unknown: the
742
+ key is gated from birth, when no manifest can newly fail the check.
743
+
744
+ **What checks the budget itself.** `doctor` measures the always tier of every workspace that declares
745
+ a `tree`, through [`../cli/context.mjs`](../cli/context.mjs), and fails it over a declared budget, or
746
+ where a declared budget cannot be judged; with none declared it reports the figure. _(This paragraph read
747
+ *"What nothing checks yet: the budget itself"* until that report landed, and it was true then:
748
+ [`a-mandate-nothing-checks-is-already-broken.md`](../.portulan/memory/a-mandate-nothing-checks-is-already-broken.md)
749
+ is why no workspace here declared the key before something checked it.)_ No workspace in this tree
750
+ declares it now either: Portulan's own contribution is railed by this repository's recipes, not by a
751
+ budget in its manifest. And, as with memory, no checker establishes that a budget was not raised in the
752
+ change that breached it.
753
+
754
+ ## `sessions` — the host switches that change what a fresh session writes
755
+
756
+ Added at **2.11**, from `0038`'s rules 3 and 4 and row 12's cache-lifetime compile target.
757
+ [`sessions.md`](../core/operating/sessions.md) is the doctrine; this object is its machine half.
758
+
759
+ | Field | What it is |
760
+ |---|---|
761
+ | `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`. |
762
+ | `cache_lifetime` | `"5m"` or `"1h"`: the main conversation's cache lifetime, compiled as Claude Code's `promptCacheTtl`. |
763
+ | `headless.git_instructions` | The same switch, for the sessions Portulan's own runners start. |
764
+ | `headless.cache_lifetime` | The same lifetime, for those sessions. |
765
+ | `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. |
766
+
767
+ ```json
768
+ "sessions": {
769
+ "headless": { "cache_lifetime": "5m", "exclude_dynamic_sections": true }
770
+ }
771
+ ```
772
+
773
+ **Nothing is defaulted.** An undeclared switch is the host's own default and `compile` emits nothing for
774
+ it, so a manifest without the key compiles byte for byte as before. A switch is declared once an A/B run
775
+ has shown that it cuts spend with no loss, and [`sessions.md`](../core/operating/sessions.md) says which
776
+ sessions keep what a switch removes.
777
+
778
+ **Two scopes, because the host has two.** `git_instructions` and `cache_lifetime` are project settings on
779
+ Claude Code, so `compile` writes them into the settings it already generates from `gates`, and they reach
780
+ every session there, interactive or not. The exclusion is no setting at all: Claude Code 2.1.281 takes it
781
+ only as a command-line flag or an SDK option, so only whatever starts a session can ask for it. `headless`
782
+ is therefore read by Portulan's own runners that start sessions, today [`../cli/warm.mjs`](../cli/warm.mjs),
783
+ which the package does not ship, and never compiled. A workspace with no gate policy has no settings
784
+ for the first two to ride, and `compile` says that it compiled neither.
785
+
786
+ **Offered to an adopter, and still never defaulted** (2026-09-24). `init` offers `cache_lifetime` `"5m"`
787
+ with what it saves and what a pause costs, asking once at a terminal or taking `--cache-lifetime`, and on
788
+ a yes drafts the key at 2.11 for `compile` to write; `upgrade` prints the same offer to a `repository`
789
+ workspace that declares no lifetime, and writes nothing; `doctor` reports where every switch stands in one
790
+ line. Only the lifetime is offered: the git instructions and the exclusion wait on their rows of
791
+ [`../evals/ab/warm.md`](../evals/ab/warm.md).
792
+
793
+ **Checked by the schema, and gated by `doctor`.** Every field is a boolean or one of two strings, which the
794
+ subset types in full, so nothing joins `doctor`'s hand-check. `doctor` refuses the key in a manifest
795
+ declaring a version before 2.11, gated from birth as `context` was.
796
+
797
+ ## `spend` — the figures the restart threshold is computed at
798
+
799
+ Added at **2.12**, from [proposal 0038](../.portulan/proposals/0038-what-a-change-spends-is-measured.md)'s
800
+ ruling 2, *the manifest declares the multipliers*, and its ruling 3's horizon; `restart` at **2.13**, from
801
+ the same ruling's block by declaration. The proposal carries the arithmetic; this object holds the figures a
802
+ workspace computes it at, and what a crossed threshold does.
803
+
804
+ | Field | What it is |
805
+ |---|---|
806
+ | `multipliers.read` | What a cache read costs, as a multiple of an uncached input token. Above 0 and at most 1. |
807
+ | `multipliers.write["5m"]` | What a five-minute cache write costs, as the same multiple. At least 1. |
808
+ | `multipliers.write["1h"]` | What a one-hour cache write costs, as the same multiple. At least 1. |
809
+ | `horizon.requests` | The requests still to go at which the restart threshold is judged. A positive integer. |
810
+ | `restart` | `"advise"` or `"block"`: what a crossed threshold does. Undeclared, `"advise"`. |
811
+
812
+ ```json
813
+ "spend": {
814
+ "multipliers": { "read": 0.05, "write": { "5m": 1.25, "1h": 2 } },
815
+ "horizon": { "requests": 30 },
816
+ "restart": "block"
817
+ }
818
+ ```
819
+
820
+ **What reads it.** A session is told to end into a handoff once its context passes
821
+ `C* ≈ F × (1 + m_w / (n × m_r))`: `F` the fresh context, `m_r` the read multiplier, `m_w` the write multiplier
822
+ at the lifetime the host recorded for the session's writes (the five-minute one where the records do not say),
823
+ and `n` the horizon. [`../cli/ledger.mjs`](../cli/ledger.mjs) reads the key from `<dir>/workspace.json` when
824
+ run with `--workspace <dir>`, and computes every threshold its report states at it. The restart advisory,
825
+ [`../cli/advisory.mjs`](../cli/advisory.mjs), is handed its command and the host's payload and nothing else,
826
+ so `compile` writes the figures onto each of its commands, the `PostToolUse` and `UserPromptSubmit` hooks, the
827
+ status line and, where `restart` declares it, the block, as `--read`, `--write-5m`, `--write-1h` and
828
+ `--horizon`, and its drift check holds them to the manifest: an edit to `spend` is drift until recompiled. A
829
+ workspace with no gate policy has no settings for them to ride, and `compile` says that it compiled nothing.
830
+ Every threshold the ledger or the advisory prints says whether its multipliers are declared, and so does a
831
+ ledger report with no threshold to judge.
832
+
833
+ **Declared, not defaulted.** Undeclared, both price at the general multipliers, a read at a tenth and writes
834
+ at 1.25× and 2×, over a horizon of 20 requests, and say `undeclared`. That is ruling 2's answer to a spread no
835
+ one figure fits: a model's cache reads cost between a fortieth and a tenth of an uncached input token. The
836
+ general tenth puts the restart line early, which is the cheaper way to err, since a restart that comes too
837
+ soon costs a fresh write and a handoff, and erring the other way, where reads cost a tenth, costs about four
838
+ times more in excess reads over 20 requests. A declaration makes a team's contracted rates and its host's real
839
+ prices one value. Either half may be declared alone, and the other keeps its general figures. The three
840
+ multipliers are one set, because a threshold priced by one declared figure and two general ones would be
841
+ priced by nobody's.
842
+
843
+ **`restart`: a block, only where declared.** `"advise"` is the advisory every workspace has: the line once,
844
+ with a tool result or at a prompt. `"block"` keeps that line and adds a block at the end of a turn: at the
845
+ first stop whose last recorded request is at or past the threshold and that no block provoked, the advisory's
846
+ own `stop` command, which `compile` writes as a second `Stop` command beside the Stop-gate's, holds the turn's
847
+ end with the line as its reason, once in a session and again after each compaction, and the turn goes on. A
848
+ stop that a block provoked is never held, since a host that gives that retry a new session id would find no
849
+ record of the hold and hold it again. It is the advisory's command rather than a reason inside the Stop-gate,
850
+ so the gate's counters and caps stay about the recipe and the handoff; and nothing ends the session, which
851
+ stays the agent's or the human's act. `0038`'s *Enforcement* says the block comes *instead* of the line, and
852
+ it is read here as *added*, because the line is what reaches an agent mid-stretch and a headless run's only
853
+ stop is its last (the coordinator session's delegated call of 2026-09-24); where the agent already acted on
854
+ the line, the block costs one request. Ruling 3 makes the block a workspace's own choice, so `init` asks
855
+ nothing about it and `upgrade` offers nothing: the threshold is an estimate, and one priced at the general
856
+ multipliers comes early for a model whose cache reads cost less than a tenth, where a block would hold a turn
857
+ short of its real line. Undeclared or `"advise"`, the settings compile byte for byte as before; where
858
+ `"block"` writes the command, `compile` says so on every run.
859
+
860
+ **What is not here.** Ruling 2 has `init` offer the declaration from the host's own pricing setting where the
861
+ host exposes one, else from a dated per-host table in the engine. Both are keyed by the model a host records,
862
+ so both wait for their own change: until then `init` offers no figures, and a workspace writes its own by
863
+ hand. Nothing checks a declared figure against what the workspace is billed; keeping it true is the declaring
864
+ workspace's work.
865
+
866
+ **Checked by the schema and by `doctor`, and refused by every reader.** The schema holds the shape: both
867
+ halves of `multipliers`, both lifetimes of `write`, and no key it does not name. The subset types the four
868
+ figures only as `number`, so `doctor` holds the read above 0 and at most 1, each write at least 1 and the
869
+ horizon to a positive integer by hand: a token read from cache costs something and never more than the same
870
+ token sent uncached, and writing one costs at least what sending it uncached does. Figures each in range can
871
+ still give no threshold, since it divides each write by the read, so `doctor` also refuses a write the read
872
+ divides past the largest number, which would put the restart line at Infinity, where no session reaches it.
873
+ The ledger, `compile` and the advisory hold the same ranges themselves rather than trusting that `doctor` ran.
874
+ The first two stop with exit 2; the advisory, whose exit 2 would erase the person's prompt, says once on
875
+ stderr what it could not use and prices that half at the general figures. A threshold that a session's fresh
876
+ context still carries past the largest number is none: the ledger stops with exit 2, and the advisory says it
877
+ is not known. `doctor` refuses the key in a manifest declaring a version before 2.12, gated from birth as
878
+ `sessions` was, and `restart` in one before 2.13. `compile` refuses `restart` there too, with exit 2 and
879
+ nothing written, so that no block is compiled from a declaration its own version's validator refuses.
880
+ `restart` is one of two strings, which the subset types in full, and the ledger and `compile` refuse any other
881
+ with exit 2.
882
+
883
+ ## `slots.context` — the guidance a host loads, each unit in its tier
884
+
885
+ Added at **2.10**, with proposal `0036`'s compile targets. [`context.md`](../core/operating/context.md)
886
+ defines the four tiers. This slot's pair is `context` above: the slot holds the guidance and the key its
887
+ budget, as `slots.memory` holds the records and `memory` their configuration.
888
+
889
+ A directory of Markdown files, one unit of guidance each; only the `.md` files at its top level are units,
890
+ and anything else in it is left alone. The file's name, less `.md`, is the unit's name and must be a slug,
891
+ because a host names a skill by it. Each file opens with frontmatter naming its tier:
892
+
893
+ ```markdown
894
+ ---
895
+ tier: on-path
896
+ paths: ["api/**", "migrations/*.sql"]
897
+ description: How this team writes an API handler and a migration.
898
+ ---
899
+
900
+ The guidance itself, which is what the host loads.
901
+ ```
902
+
903
+ | Key | What it is |
904
+ |---|---|
905
+ | `tier` | `always`, `on-path`, `on-invoke` or `on-read`: `0036`'s four tiers, spelled as that proposal spells them. |
906
+ | `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. |
907
+ | `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. |
908
+
909
+ **The tier is declared, never inferred**, and a unit sits in one tier, so what a unit costs is what its
910
+ tier costs and nothing has to read the file to find out where it belongs.
911
+
912
+ **`compile` emits it, per host.** For Claude Code, an `always` unit is an unscoped rule, loaded into every
913
+ context; an `on-path` unit is a rule scoped by `paths:`; an `on-invoke` unit is a project skill, whose
914
+ description is always loaded and whose body loads when it is invoked; and an `on-read` unit is one line in
915
+ an unscoped index of pointers, naming its file, its size in whole KB and its description (the size since
916
+ 2026-09-24, so a session knows what it passes over), while its file stays where it is for the agent to
917
+ open. **A tier a host cannot express degrades to an on-read pointer, never to nothing**: the vendored
918
+ `AGENTS.md` carries the `always` units inline and every other unit as a pointer, one level deep. The
919
+ compiled files are committed and byte-compared against this slot, as the compiled settings are against
920
+ `gates`, so a desktop session's fresh worktree has them. `compile` shows which rules in
921
+ `.claude/rules/portulan/` are its own with a marker there, `.compiled`, a file no host loads that lists
922
+ each rule it wrote: it rewrites or removes only a rule the marker lists, stops with exit 2 rather than
923
+ replace one it does not list, and stops too where the directory holds Markdown files and no marker in its
924
+ form. It writes and removes nothing through a link.
925
+
926
+ **Checked by `compile`, not by the schema or `doctor`.** The subset cannot see inside a file, so the
927
+ frontmatter is its consumer's to refuse: an unknown key, a tier outside the four, `paths` on a unit that is
928
+ not on-path or reaching outside the repository, a missing `description` or one on an `always` unit, a unit
929
+ with no guidance under its frontmatter, a link out of the workspace or into a directory `compile` writes into,
930
+ a slot in one of those (`.claude/`, or the workspace's `compile/`, neither of which `vendor` carries), a name
931
+ that is not a slug or is `on-read`, which the index takes, or a unit that would replace a skill written by
932
+ hand or compiled from another unit stops `compile` with exit 2, *could not compile*. `doctor` checks what it
933
+ checks of every directory slot, that the path resolves, and refuses the slot in a manifest declaring a version
934
+ before 2.10, gated from birth as `context` was.
935
+
936
+ **A unit named `boot` is the workspace's boot card**, reserved within 2.10, with no key and no version
937
+ bump (decided 2026-09-23). It is an `always` unit whose first line
938
+ under the frontmatter is `# Portulan boot card`, and the boot skill reads it in place of the slots when
939
+ that line is in its context. `compile` refuses a `boot` unit in another tier or opening with another line,
940
+ and a unit of another name opening with that one, which the skill would take for the card.
941
+
942
+ **A section of a project instruction file can become an on-read unit**, with no key and no version bump
943
+ (2026-09-24). A team marks it with a line `<!-- portulan: on-read -->` directly under its heading in
944
+ `CLAUDE.md` or `.claude/CLAUDE.md`, and `upgrade`'s form step `0009` moves the section, heading and all
945
+ and byte for byte, into a unit of this slot named by the heading, whose `description` is the heading's own
946
+ text, leaving `<!-- portulan: on-read <unit> <digest> -->` where it was, the digest the moved section's.
947
+ The file must reassemble from its units byte for byte, or nothing moves. Claude Code drops both comments
948
+ before it loads the file, so neither costs a context. The unit's text is not rewritten, so a relative link
949
+ in it still reads from the repository root, as it did in `CLAUDE.md`.
950
+ [`../cli/instructions.mjs`](../cli/instructions.mjs) is the same split on the command line, and `--join`
951
+ puts every section back as it stands, saying which one the digest shows edited since, and refusing a unit
952
+ re-tiered since, or a file not at the top of this slot, rather than undo its tier or remove it.
953
+
954
+ **An `always` unit may import whole files and write out another file's lead sentences or a gate policy's
955
+ gates**, so a card carries a fact without holding a second copy of it:
956
+
957
+ - **An import is `@` and a path, alone on its line, relative to the unit's own file.** Claude Code 2.1.281
958
+ loads a rule as it loads `CLAUDE.md`: it resolves an import against the file making it, follows it only
959
+ inside the project, skips code blocks, skips code spans and HTML comments except in a list item's text,
960
+ which it reads whole (2026-09-24), and loads nothing five imports below the rule. A rule scoped by
961
+ `paths:` loads on its path, and the files it imports load into every context, since an
962
+ imported file carries no `paths:` of its own (read in the program text, and seen on a fixture). So
963
+ `compile` spells each import again from the rule it compiles to, and refuses one that is a home or an
964
+ absolute path, leaves or links out of the tree it compiles, names no file, shares its line, or sits five
965
+ imports down; in a unit of any other tier it refuses an import naming a file, which would not load as
966
+ its unit does. `context.mjs` counts an imported file in the always tier at the depth the host loads it,
967
+ a path-scoped rule's included. The vendored `AGENTS.md` carries each import as a pointer line to the
968
+ file in the vendored tree, and one for each file that file imports in turn, to the depth the host would
969
+ load (2026-09-24), and `vendor` refuses an import leaving the workspace, which is all that tree holds.
970
+ - **A line `<!-- leads: <path> -->`, alone on its line, is replaced by the first sentence of each item in
971
+ the first list of the file it names**, each item opening with a bold lead. The file stays the one
972
+ source of those sentences, so a change there is drift until the unit is recompiled. `compile` refuses
973
+ a file with no list, an item with no bold lead, a lead carrying a link, which would not resolve from
974
+ the compiled rule, and a line of text with no indent straight under an item, which CommonMark reads as
975
+ more of that item (2026-09-24). **The path may end `#<heading>`**, for the first list under that
976
+ heading, found as `cli/symbols.mjs` finds a section: by the anchor GitHub gives it, which its outline
977
+ prints, or by its text or a `Parent>Child` path where either holds no space, since the line's path holds
978
+ none; a heading that none or several answer is refused (2026-09-24).
979
+ - **A line `<!-- engine: <path> -->`, alone on its line, writes out the leads of a file in the engine
980
+ `compile` ships with**, its path relative to that engine's `core/`, and refused outside it or naming no
981
+ file, with a `#<heading>` read as above (2026-09-24, within 2.10). Portulan's own card and each
982
+ consumer's name the same file, so they carry one text, and an upgrade of Portulan that changes it is
983
+ drift until the card is recompiled. `<plugin root>/` in a lead is the directory the plugin is installed
984
+ in, and is written as nothing where that engine is the tree compiled, as in Portulan's own repository.
985
+ - **A line `<!-- gates: <path> -->`, alone on its line, is replaced by the gate ids of the policy it
986
+ names, one line per tier** in core's order, each id as the policy spells it and in its order there, a
987
+ tier holding none saying so, and a line naming the packs the manifest composes, whose gates only a
988
+ compile resolving them can list (2026-09-24). The policy is read by the reader every compiled gate goes
989
+ through, so `compile` refuses here what it refuses there, and an edit to the policy is drift until the
990
+ unit is recompiled. `init` and `upgrade` draft a consumer's card with one.
991
+
992
+ **Nothing is defaulted.** A workspace that declares no slot compiles no guidance, and no directory in an
993
+ adopter's workspace is chosen by a key nobody typed.
994
+
683
995
  ## `provenance` — a record field, not a manifest key
684
996
 
685
997
  The criterion lists a provenance slot beside the others, but it does not belong in the manifest, and