@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.
- package/README.md +6 -5
- package/cli/README.md +142 -358
- package/cli/advisory.mjs +354 -0
- package/cli/comments.mjs +932 -0
- package/cli/compile.mjs +1322 -1607
- package/cli/context.mjs +883 -0
- package/cli/control-chars.mjs +12 -301
- package/cli/discover.mjs +17 -574
- package/cli/doctor.mjs +224 -1074
- package/cli/drills.mjs +68 -502
- package/cli/feedback.mjs +16 -237
- package/cli/finish.mjs +437 -0
- package/cli/form.mjs +644 -0
- package/cli/fuzz-shell.mjs +20 -305
- package/cli/gate.mjs +7 -199
- package/cli/goldens.mjs +26 -184
- package/cli/index.mjs +305 -655
- package/cli/init.mjs +243 -566
- package/cli/inside.mjs +3 -28
- package/cli/instructions.mjs +829 -0
- package/cli/ledger.mjs +778 -0
- package/cli/librarian.mjs +135 -599
- package/cli/manifest.mjs +2 -39
- package/cli/mutants.mjs +9 -170
- package/cli/new.mjs +12 -243
- package/cli/pack-identity.mjs +5 -74
- package/cli/pack-version.mjs +13 -255
- package/cli/plugin-lint.mjs +42 -658
- package/cli/portulan.mjs +5 -124
- package/cli/recipe-set.mjs +15 -245
- package/cli/release-eval.mjs +28 -348
- package/cli/review-meter.mjs +17 -258
- package/cli/rule-carriers.mjs +12 -175
- package/cli/sessions.mjs +82 -0
- package/cli/skill-goldens.mjs +10 -176
- package/cli/skills-set.mjs +12 -414
- package/cli/stop-gate.mjs +83 -514
- package/cli/symbols.mjs +1132 -0
- package/cli/telemetry.mjs +27 -492
- package/cli/upgrade.mjs +171 -314
- package/cli/vendor.mjs +146 -476
- package/cli/version-carriers.mjs +8 -86
- package/core/README.md +1 -1
- package/core/engine.md +3 -9
- package/core/operating/autonomy.md +1 -1
- package/core/operating/context.md +183 -0
- package/core/operating/evolution.md +3 -3
- package/core/operating/loop.md +13 -20
- package/core/operating/memory.md +12 -3
- package/core/operating/sessions.md +100 -0
- package/core/personas/librarian.md +2 -2
- package/core/skills/consolidate/SKILL.md +4 -2
- package/core/templates/handoff.md +28 -15
- package/core/templates/memory-entry.md +1 -0
- package/evals/README.md +255 -845
- package/evals/releases/0.2.0.json +146 -0
- package/evals/releases/0.2.0.md +67 -0
- package/evals/releases/README.md +3 -3
- package/package.json +4 -2
- package/packs/rituals/checkpoints/README.md +1 -1
- package/packs/tools/github/verify/actions-pinned.sh +10 -55
- package/spec/README.md +112 -6
- package/spec/migrations/0001-repository-declares-its-tree.mjs +1 -26
- package/spec/migrations/0002-bundle-fallback-path.mjs +13 -74
- package/spec/migrations/0003-handoff-index-not-kept.mjs +66 -0
- package/spec/migrations/0004-changelog-fragments.mjs +53 -0
- package/spec/migrations/0005-session-log-retired.mjs +69 -0
- package/spec/migrations/0006-boot-card.mjs +77 -0
- package/spec/migrations/0007-guidance-compiled.mjs +31 -0
- package/spec/migrations/0008-card-reading.mjs +55 -0
- package/spec/migrations/0009-instruction-sections-on-read.mjs +60 -0
- package/spec/migrations/0010-comment-rail.mjs +57 -0
- package/spec/migrations/README.md +45 -10
- package/spec/pack.schema.json +1 -1
- package/spec/slots.md +333 -21
- 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
|
|
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),
|
|
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) }
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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 `
|
|
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
|
|
package/spec/pack.schema.json
CHANGED
|
@@ -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/
|
|
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) —
|
|
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
|
|
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
|
|
327
|
-
|
|
328
|
-
per-product from the start, not a workspace-level field that a
|
|
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
|
|
354
|
-
slot holds what an agent can rely on — entry
|
|
355
|
-
not assume, which is the half that makes
|
|
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
|
|
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
|
|
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
|
|
588
|
-
|
|
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
|
|
596
|
-
|
|
597
|
-
the
|
|
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
|