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