@sleepy_panda_srl/portulan 0.3.0 → 0.4.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 +15 -4
- package/cli/README.md +55 -3
- package/cli/advisory.mjs +167 -53
- package/cli/cache-risk.mjs +69 -0
- package/cli/comments.mjs +8 -5
- package/cli/compile.mjs +492 -146
- package/cli/context.mjs +204 -43
- package/cli/doctor.mjs +211 -33
- package/cli/drills.mjs +32 -7
- package/cli/finish.md +34 -0
- package/cli/finish.mjs +8 -67
- package/cli/form.mjs +34 -18
- package/cli/frontmatter.mjs +39 -0
- package/cli/gate.mjs +14 -12
- package/cli/guidance-files.mjs +335 -0
- package/cli/handoff-policy.mjs +76 -0
- package/cli/html-entities.mjs +2130 -0
- package/cli/import-files.mjs +15 -0
- package/cli/index.mjs +77 -24
- package/cli/init-preview.mjs +553 -0
- package/cli/init.mjs +261 -46
- package/cli/instructions-journal.mjs +1185 -0
- package/cli/instructions.mjs +332 -57
- package/cli/ledger.mjs +123 -37
- package/cli/librarian.mjs +103 -19
- package/cli/persona-routing.mjs +209 -0
- package/cli/plugin-lint.mjs +77 -56
- package/cli/recipe-output.mjs +91 -0
- package/cli/recipe-runner.md +68 -0
- package/cli/recipe-runner.mjs +131 -0
- package/cli/recipe-set.mjs +12 -3
- package/cli/ritual-contexts.mjs +103 -0
- package/cli/sessions.mjs +2 -1
- package/cli/spend-analysis.mjs +111 -0
- package/cli/spend-offer.md +53 -0
- package/cli/spend-offer.mjs +93 -0
- package/cli/stop-diagnostics.mjs +114 -0
- package/cli/stop-gate.mjs +133 -90
- package/cli/stop-policy.mjs +14 -0
- package/cli/symbols.mjs +55 -8
- package/cli/telemetry.mjs +19 -12
- package/cli/upgrade.mjs +62 -44
- package/cli/vendor.mjs +207 -125
- package/core/operating/context.md +368 -60
- package/core/operating/evolution.md +3 -2
- package/core/operating/loop.md +9 -4
- package/core/operating/sessions.md +6 -15
- package/core/personas/README.md +52 -10
- package/core/personas/implementer.md +3 -0
- package/core/personas/librarian.md +3 -0
- package/core/personas/reviewer.md +3 -0
- package/core/templates/handoff.md +6 -2
- package/evals/ab/personas/README.md +111 -0
- package/evals/ab/personas/acquisition.json +330 -0
- package/evals/ab/personas/bindings/implementer.txt +43 -0
- package/evals/ab/personas/bindings/librarian.txt +40 -0
- package/evals/ab/personas/bindings/reviewer.txt +41 -0
- package/evals/ab/personas/corpus.json +69 -0
- package/evals/ab/personas/grader.mjs +171 -0
- package/evals/ab/personas/implementer.json +774 -0
- package/evals/ab/personas/implementer.md +22 -0
- package/evals/ab/personas/librarian.json +758 -0
- package/evals/ab/personas/librarian.md +22 -0
- package/evals/ab/personas/protocol/continuation.mjs.txt +284 -0
- package/evals/ab/personas/protocol/first-outcome.mjs.txt +227 -0
- package/evals/ab/personas/protocol/first-probe.mjs.txt +172 -0
- package/evals/ab/personas/protocol/publisher.mjs.txt +126 -0
- package/evals/ab/personas/protocol/second-probe.mjs.txt +193 -0
- package/evals/ab/personas/repairs/first-control-answer.txt +23 -0
- package/evals/ab/personas/repairs/grader-before-decode-repair.mjs.txt +143 -0
- package/evals/ab/personas/reviewer.json +758 -0
- package/evals/ab/personas/reviewer.md +22 -0
- package/evals/releases/0.4.0.json +154 -0
- package/evals/releases/0.4.0.md +69 -0
- package/package.json +6 -2
- package/packs/rituals/checkpoints/README.md +35 -0
- package/packs/rituals/checkpoints/pack.json +3 -2
- package/packs/tools/local-llm/README.md +46 -6
- package/packs/tools/local-llm/runner/local-llm.mjs +127 -13
- package/packs/tools/local-llm/skills/delegate-local/SKILL.md +26 -2
- package/spec/README.md +37 -4
- package/spec/migrations/0009-instruction-sections-on-read.mjs +60 -11
- package/spec/migrations/0011-persona-bindings-compiled.mjs +23 -0
- package/spec/migrations/README.md +6 -0
- package/spec/pack.schema.json +12 -3
- package/spec/slots.md +262 -117
- package/spec/workspace.schema.json +67 -11
package/spec/slots.md
CHANGED
|
@@ -50,7 +50,7 @@ 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) — when declared, the Stop-gate checks this series for open work; when absent, it asks to commit and push without a handoff |
|
|
54
54
|
| `slots.proposals` | path | no | content — [`proposals/`](../.portulan/proposals/); [`evolution.md`](../core/operating/evolution.md) |
|
|
55
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 |
|
|
56
56
|
| `verify` | structured | **for a governing kind** | criterion — *verify recipes*; [`verification.md`](../core/operating/verification.md) — the workspace sets the default |
|
|
@@ -308,12 +308,21 @@ demonstration that a structured slot earns being structured. Recorded in custome
|
|
|
308
308
|
```json
|
|
309
309
|
"verify": {
|
|
310
310
|
"default": "{recipe-id}",
|
|
311
|
+
"stopTimeout": 360,
|
|
311
312
|
"recipes": [
|
|
312
313
|
{ "id": "{recipe-id}", "run": "{command}", "requires": ["{tool}"], "doc": "{path}" }
|
|
313
314
|
]
|
|
314
315
|
}
|
|
315
316
|
```
|
|
316
317
|
|
|
318
|
+
`stopTimeout` is optional, added in 2.14: the Stop recipe deadline in seconds,
|
|
319
|
+
undeclared at 90. A whole number from 1 through 2,147,423 keeps both Node timers
|
|
320
|
+
in range. `doctor`, `compile` and the Stop runner refuse invalid values and
|
|
321
|
+
declarations below 2.14. Recompile after changing it: the outer Stop hook gets
|
|
322
|
+
sixty additional seconds. Both command and deadline come from the trusted
|
|
323
|
+
hook-told workspace; a payload cannot select executable policy. See
|
|
324
|
+
[execution and cap recovery](../cli/recipe-runner.md).
|
|
325
|
+
|
|
317
326
|
`requires` exists because of an exit code. The recipes distinguish *red* (exit 1, the check ran and
|
|
318
327
|
failed) from *could not run* (exit 2, a tool is missing), and that distinction is worthless if nothing
|
|
319
328
|
declares what was needed. Customer zero now has two recipes with different needs — one POSIX-only, one
|
|
@@ -574,69 +583,50 @@ behind it, rather than left for a reader to assume the rail covers both halves.
|
|
|
574
583
|
earlier, or later without a tighter cap, is the same hand edit, since it unbinds what the cap bound,
|
|
575
584
|
and the same gate holds it for the same reason.
|
|
576
585
|
|
|
577
|
-
## `handoffs` — an
|
|
586
|
+
## `handoffs` — an individual cap and a generated window
|
|
578
587
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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.
|
|
588
|
+
`slots.handoffs` names the append-only series. `handoffs.index.path`, required whenever `handoffs`
|
|
589
|
+
is present, names the derived index outside that series. A kept copy is byte-compared and regenerated;
|
|
590
|
+
with none, `index --handoffs` prints it on demand. Without budgets the full series renders as before.
|
|
585
591
|
|
|
586
592
|
| Field | What it is |
|
|
587
593
|
|---|---|
|
|
588
|
-
| `
|
|
589
|
-
|
|
590
|
-
`index`
|
|
591
|
-
`
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
remedy such a budget could ask for is barred, which makes it a rail designed to be broken — and a rail
|
|
625
|
-
that fires with no legal repair is how a whole recipe gets switched off.
|
|
626
|
-
|
|
627
|
-
_That test is the one the store's own aggregate byte budget later failed — which is why **this
|
|
628
|
-
repository's workspace** swapped `kilobytes` for the per-record cap at 2.8 rather than widening it. The
|
|
629
|
-
definition replaced nothing: both keys stay legal and a workspace may declare either. The argument this
|
|
630
|
-
paragraph makes about handoffs is the argument `0025` makes about `kilobytes`, and it was written down
|
|
631
|
-
before the case arrived._ Whether
|
|
632
|
-
the series should be railed on some other axis is a **maintainer's question**, deferred to the
|
|
633
|
-
reconciliation that follows milestone 5 rather than pre-answered here in the shape of an unused key.
|
|
634
|
-
|
|
635
|
-
**What the pass reads that this object does not configure.** Record ages come from git for both series,
|
|
636
|
-
and the handoff series is **reported** — count, oldest, total size — with no threshold and no demotion
|
|
637
|
-
draft. That is not an omission: `librarian.staleness.record_days` drafts a demotion, and drafting one
|
|
638
|
-
against an append-only series would recommend the single repair the paragraph above rules out, weekly,
|
|
639
|
-
forever.
|
|
594
|
+
| `series.budget.record_tokens` | Optional positive integer cap, added in 2.15, counted at explicit `context.ratio`. |
|
|
595
|
+
| `series.budget.cutoff` | Required real YYYY-MM-DD date beside the cap; only later handoffs are bound. |
|
|
596
|
+
| `index.path` | Generated index path, outside `slots.handoffs`. |
|
|
597
|
+
| `index.budget.tokens` | Optional positive integer budget on the complete rendered index, added in 2.15. |
|
|
598
|
+
|
|
599
|
+
Accepted [proposal 0037](../.portulan/proposals/0037-a-handoff-is-budgeted-where-it-is-written.md)
|
|
600
|
+
keeps the series unbudgeted: deleting history cannot repair a size rail. An individual new handoff can
|
|
601
|
+
still be compressed before merge, or facts moved to their owning records and linked. Never split a
|
|
602
|
+
handoff, delete a decision's why, raise the cap or move the cutoff to repair a breach. The cutoff is set
|
|
603
|
+
at the declaring change's date or newest merged handoff's date, whichever is later; the cutoff day
|
|
604
|
+
itself stays unbound. Tightening the cap or lowering the ratio may advance it to exempt merged history;
|
|
605
|
+
it never moves backward. Review holds these history-dependent rules and prevents backdating new work.
|
|
606
|
+
|
|
607
|
+
`index` checks filename dates wherever a series is declared, with or without an index or cap: a real
|
|
608
|
+
YYYY-MM-DD prefix, no more than one day beyond UTC today. An H1 is required only when rendering an
|
|
609
|
+
index; titles come from that heading, dates from filenames. Each handoff's full UTF-8 byte length,
|
|
610
|
+
corrections included, is converted with the same rounded bytes/ratio estimate as the context report.
|
|
611
|
+
Every bound overage is red, with its count, cap, excess and repair; pre-cutoff overages are notes.
|
|
612
|
+
Either token budget without an explicit ratio is refused by doctor and direct index calls.
|
|
613
|
+
|
|
614
|
+
A budgeted index lists a contiguous newest-first prefix of whole lines. Header, separators, omission
|
|
615
|
+
notice and trailing newline all count. The notice names the omitted count and source directory; no
|
|
616
|
+
source record moves or disappears and no second index level exists. If the newest entry cannot fit,
|
|
617
|
+
none are listed. A budget too small even for the header and notice is refused with its minimum.
|
|
618
|
+
Neither series nor index is part of a boot or the always tier. Context and doctor report count, total,
|
|
619
|
+
median, the three largest and rendered index bytes/tokens even without a tree, naming estimated or
|
|
620
|
+
declared ratio and bound versus historical overages. Init offers independently optional 3000-token
|
|
621
|
+
budgets, requires a supplied ratio on acceptance and computes the cutoff at write time from the same
|
|
622
|
+
regular Markdown entries and filename-date boundary the index uses. Unrelated files cannot advance it;
|
|
623
|
+
invalid selected dates or file types, or an uninspectable series, stop record-cap adoption before
|
|
624
|
+
writing. Supplied ratio provenance is not proof of host calibration. Upgrade adopts neither policy
|
|
625
|
+
nor ratio automatically.
|
|
626
|
+
|
|
627
|
+
The librarian still ages the series from git and drafts no staleness demotion against it: retiring
|
|
628
|
+
history is no repair. The session-end gate checks existence and today's date only; the cap is the
|
|
629
|
+
index verify rail. The one-day cutoff gap and history-dependent no-raise rules remain review duties.
|
|
640
630
|
|
|
641
631
|
## `personas` — where a pack-declared memory scope lands, and how anyone can tell it did
|
|
642
632
|
|
|
@@ -713,8 +703,8 @@ one source for what each load tier holds; this object is its machine half, as `m
|
|
|
713
703
|
| Field | What it is |
|
|
714
704
|
|---|---|
|
|
715
705
|
| `always.budget.tokens` | The most tokens the always tier may load, counted at the ratio below. |
|
|
716
|
-
| `ratio.bytes_per_token` |
|
|
717
|
-
| `ratio.calibrated_by` | The host
|
|
706
|
+
| `ratio.bytes_per_token` | Manually declared bytes per token for the host's measured domain. At least 1; provider estimates are qualified below. |
|
|
707
|
+
| `ratio.calibrated_by` | The declared compile-host/domain ID. Doctor matches `claude-code` or `agents-md` to its reported format; a different or unknown valid ID is labeled as extrapolated. Provider/model provenance belongs in the count report. |
|
|
718
708
|
|
|
719
709
|
```json
|
|
720
710
|
"context": {
|
|
@@ -729,30 +719,48 @@ it. Those are dependencies the subset can state, so they are the schema's and jo
|
|
|
729
719
|
conditionals. A ratio alone is a legitimate shape: it is what a report in tokens needs, and the default
|
|
730
720
|
is the report.
|
|
731
721
|
|
|
732
|
-
**Declared, not measured per run.**
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
722
|
+
**Declared, not measured per run.** On-demand counting is a network call no verify recipe may make,
|
|
723
|
+
and a ratio that moved between runs would make one tree red on one run and green on the next. There is
|
|
724
|
+
no date field, because the commit that changes the ratio dates it. One declared ratio is shared by the
|
|
725
|
+
two measured project formats. Reports compare `calibrated_by` with the format ID (`claude-code` or
|
|
726
|
+
`agents-md`); a nonmatching or unknown ID makes that use an explicit extrapolation. The declaration
|
|
727
|
+
still accepts any valid host slug. No second per-host ratio is declared.
|
|
728
|
+
|
|
729
|
+
For [#506](https://github.com/sleepy-panda-srl/portulan/issues/506), Marius accepted “Provider count with
|
|
730
|
+
stated limit” on 2026-11-09. `doctor --count-tokens MODEL_ID` and the direct context diagnostic submit
|
|
731
|
+
the measured Claude Code project corpus to Anthropic, which returns an estimate that may differ from actual host
|
|
732
|
+
usage and includes API framing. The effective ratio is approximate and manual; neither command writes
|
|
733
|
+
this object or invents a `calibrated_by` provider ID. An empty corpus produces no request or ratio.
|
|
734
|
+
The original exact-host wording is historical acceptance, not a claim of current host integration.
|
|
735
|
+
See [the counting boundary](../core/operating/context.md#the-always-tier-is-budgeted-in-tokens).
|
|
737
736
|
|
|
738
737
|
**Named for its tier.** Only the always tier is budgeted, and the budget sits under `always` so a budget
|
|
739
738
|
on a later tier would sit beside it rather than force a rename, which would be a MAJOR.
|
|
740
739
|
|
|
741
740
|
**Nothing is defaulted**, on memory's rule: an undeclared budget is not checked. `doctor` refuses a
|
|
742
741
|
declared token budget that is not a positive integer, as it refuses the memory budgets, and a ratio under
|
|
743
|
-
1,
|
|
742
|
+
1, guarding against inverted units. Provider framing can put a tiny corpus below that floor; such a
|
|
743
|
+
ratio is reported but cannot be declared.
|
|
744
744
|
Both are `doctor`'s because the subset has no `minimum` and cannot say `integer`. `doctor` also refuses
|
|
745
745
|
`context` in a manifest declaring a version before 2.9, whose own validator refuses it as unknown: the
|
|
746
746
|
key is gated from birth, when no manifest can newly fail the check.
|
|
747
747
|
|
|
748
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),
|
|
750
|
-
|
|
749
|
+
a `tree`, through [`../cli/context.mjs`](../cli/context.mjs), for Claude Code and root vendored AGENTS.md.
|
|
750
|
+
Doctor reports both automatically and judges the same declared cap independently for each, never
|
|
751
|
+
summing separate contexts. AGENTS includes the file's inline guidance and pointer text without opening
|
|
752
|
+
targets or expanding at-sign text; personal, above-tree, nested and plugin files are excluded. An absent
|
|
753
|
+
optional root is zero; present unmeasurable sources, including an escaping AGENTS root, are reported
|
|
754
|
+
unmeasured. A declared budget then cannot be judged and fails. Without a budget, measurement does not
|
|
755
|
+
change the verdict. The default context/boot line and optional provider request stay Claude-specific;
|
|
756
|
+
doctor validates both applicable budgets before requesting that count. _(This paragraph read
|
|
751
757
|
*"What nothing checks yet: the budget itself"* until that report landed, and it was true then:
|
|
752
758
|
[`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.)_
|
|
754
|
-
declares it
|
|
755
|
-
|
|
759
|
+
is why no workspace here declared the key before something checked it.)_ Neither this repository's
|
|
760
|
+
workspace nor the demo workspace declares it: Portulan's own contribution is railed by this repository's
|
|
761
|
+
recipes, not by a budget in its manifest. The synthetic
|
|
762
|
+
[budget demonstration fixture](../cli/fixtures/context-budget/) does declare one. And, as with memory,
|
|
763
|
+
no general checker establishes that a budget was not raised in the
|
|
756
764
|
change that breached it.
|
|
757
765
|
|
|
758
766
|
## `sessions` — the host switches that change what a fresh session writes
|
|
@@ -775,17 +783,19 @@ Added at **2.11**, from `0038`'s rules 3 and 4 and row 12's cache-lifetime compi
|
|
|
775
783
|
```
|
|
776
784
|
|
|
777
785
|
**Nothing is defaulted.** An undeclared switch is the host's own default and `compile` emits nothing for
|
|
778
|
-
it, so
|
|
779
|
-
has shown that it cuts spend with no loss, and [`sessions.md`](../core/operating/sessions.md) says which
|
|
786
|
+
it, so an existing policy-backed manifest without the key keeps the same settings bytes. A switch is
|
|
787
|
+
declared once an A/B run has shown that it cuts spend with no loss, and [`sessions.md`](../core/operating/sessions.md) says which
|
|
780
788
|
sessions keep what a switch removes.
|
|
781
789
|
|
|
782
790
|
**Two scopes, because the host has two.** `git_instructions` and `cache_lifetime` are project settings on
|
|
783
|
-
Claude Code, so `compile` writes them into
|
|
791
|
+
Claude Code, so `compile` writes them into its native settings, independently of a gate policy, and they reach
|
|
784
792
|
every session there, interactive or not. The exclusion is no setting at all: Claude Code 2.1.281 takes it
|
|
785
793
|
only as a command-line flag or an SDK option, so only whatever starts a session can ask for it. `headless`
|
|
786
794
|
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.
|
|
788
|
-
|
|
795
|
+
which the package does not ship, and never compiled. Full compilation of an authored `repository`,
|
|
796
|
+
`demo` or `portfolio` workspace also emits native restart advice and these declared switches without a gate policy. The settings contain no permissions
|
|
797
|
+
or verification Stop hook; explicit `spend.restart: "block"` adds only the advisory Stop command.
|
|
798
|
+
Missing manifests and pointers do not become governing workspaces.
|
|
789
799
|
|
|
790
800
|
**Offered to an adopter, and still never defaulted** (2026-09-24). `init` offers `cache_lifetime` `"5m"`
|
|
791
801
|
with what it saves and what a pause costs, asking once at a terminal or taking `--cache-lifetime`, and on
|
|
@@ -811,6 +821,7 @@ workspace computes it at, and what a crossed threshold does.
|
|
|
811
821
|
| `multipliers.write["5m"]` | What a five-minute cache write costs, as the same multiple. At least 1. |
|
|
812
822
|
| `multipliers.write["1h"]` | What a one-hour cache write costs, as the same multiple. At least 1. |
|
|
813
823
|
| `horizon.requests` | The requests still to go at which the restart threshold is judged. A positive integer. |
|
|
824
|
+
| `resume.tokens` | Additional restart expense in uncached-input-token equivalents, finite and nonnegative; absent means unknown. Added in 2.16. |
|
|
814
825
|
| `restart` | `"advise"` or `"block"`: what a crossed threshold does. Undeclared, `"advise"`. |
|
|
815
826
|
|
|
816
827
|
```json
|
|
@@ -821,36 +832,40 @@ workspace computes it at, and what a crossed threshold does.
|
|
|
821
832
|
}
|
|
822
833
|
```
|
|
823
834
|
|
|
824
|
-
**What reads it.**
|
|
825
|
-
`C*
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
835
|
+
**What reads it.** `cli/ledger.mjs --workspace <dir>` and the compiled advisory
|
|
836
|
+
commands compute `C* = F + (F × m_w + R) / (n × m_r)`. Here `F` is the first
|
|
837
|
+
recorded context after startup or compaction, `n` requests remaining, and `R` is
|
|
838
|
+
`spend.resume.tokens` (optional since **2.16**). `R` is additional restart expense
|
|
839
|
+
in uncached-input-token equivalents, excluding the existing fresh-context write
|
|
840
|
+
and baseline reads. Include handoff output, boot/state recovery and additional
|
|
841
|
+
rereads over the selected horizon. These are weighted cost units: 2,000 output
|
|
842
|
+
tokens at 5× contribute 10,000 equivalents. A finite nonnegative number, including
|
|
843
|
+
explicit zero, is priced; absent is unknown and the reported threshold is only a
|
|
844
|
+
lower bound. No automatic predictor fills it.
|
|
845
|
+
|
|
846
|
+
`compile` writes `--read`, `--write-5m`, `--write-1h`, `--horizon` and
|
|
847
|
+
`--resume-tokens` on all advisory commands where declared, with drift checked.
|
|
848
|
+
A harness may supply its task's estimates on these commands; no task-file resolver
|
|
849
|
+
exists. A horizon is requests **remaining**, does not decrement automatically and
|
|
850
|
+
must be revised by the caller. For example, `spend.horizon: {"requests": 3}` and
|
|
851
|
+
`spend.resume: {"tokens": 10000}` price a short task's expected remainder.
|
|
852
|
+
|
|
853
|
+
**Declared, not defaulted.** Undeclared multipliers remain read 0.1× and writes
|
|
854
|
+
1.25×/2×, with a default horizon of 20. Advisory, status and ledger name the horizon
|
|
855
|
+
and its origin, the multipliers and priced/unknown resume cost. Unknown cost or a
|
|
856
|
+
default horizon produces conditional advice, never a claim that restarting must
|
|
857
|
+
cost less. The caller supplies estimates; no configuration is silently installed.
|
|
846
858
|
|
|
847
859
|
**`restart`: a block, only where declared.** `"advise"` is the advisory every workspace has: the line once,
|
|
848
860
|
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
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
861
|
+
first stop with an explicit horizon and resume expense, finite strictly positive
|
|
862
|
+
estimated savings `n × read × (C − F) − write × F − R`, and no preceding hook
|
|
863
|
+
block. Equality never holds, and comparison uses unrounded arithmetic rather than
|
|
864
|
+
the displayed whole-token threshold. Without both estimates an existing block
|
|
865
|
+
declaration reports conditionally without holding. Retry protection uses the
|
|
866
|
+
host's `stop_hook_active` marker; omitting it while rotating session identities
|
|
867
|
+
can repeat holds. A hold is once per session and again after each compaction.
|
|
868
|
+
It is the advisory's command rather than a reason inside the Stop-gate,
|
|
854
869
|
so the gate's counters and caps stay about the recipe and the handoff; and nothing ends the session, which
|
|
855
870
|
stays the agent's or the human's act. `0038`'s *Enforcement* says the block comes *instead* of the line, and
|
|
856
871
|
it is read here as *added*, because the line is what reaches an agent mid-stretch and a headless run's only
|
|
@@ -858,17 +873,20 @@ stop is its last (the coordinator session's delegated call of 2026-09-24); where
|
|
|
858
873
|
the line, the block costs one request. Ruling 3 makes the block a workspace's own choice, so `init` asks
|
|
859
874
|
nothing about it and `upgrade` offers nothing: the threshold is an estimate, and one priced at the general
|
|
860
875
|
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.
|
|
876
|
+
short of its real line. In the existing policy-backed path, undeclared or `"advise"` settings retain their bytes; where
|
|
862
877
|
`"block"` writes the command, `compile` says so on every run.
|
|
863
878
|
|
|
864
|
-
**
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
879
|
+
**Offered by `init`, never written without acceptance.** For Claude Code's last recorded main-session
|
|
880
|
+
model in this repository, `init` offers a representable file-based managed `modelPricing` override,
|
|
881
|
+
else its dated vendor table, else the general multipliers. It offers the horizon independently as
|
|
882
|
+
requests still to go. `--multipliers '<JSON>'` and `--horizon <requests>` (or the same string answers in
|
|
883
|
+
`--answers`) declare either key without an interview; a pointer refuses both. Accepting either drafts
|
|
884
|
+
spec 2.12. The [pricing offer](../cli/spend-offer.md) names the supported IDs, matching and file-source
|
|
885
|
+
limits. No network request is made. Nothing checks a declaration against a bill or a future model;
|
|
886
|
+
keeping it true is the declaring workspace's work.
|
|
869
887
|
|
|
870
888
|
**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
|
|
889
|
+
halves of `multipliers`, both lifetimes of `write`, and no key it does not name. The subset types the numeric
|
|
872
890
|
figures only as `number`, so `doctor` holds the read above 0 and at most 1, each write at least 1 and the
|
|
873
891
|
horizon to a positive integer by hand: a token read from cache costs something and never more than the same
|
|
874
892
|
token sent uncached, and writing one costs at least what sending it uncached does. Figures each in range can
|
|
@@ -876,11 +894,15 @@ still give no threshold, since it divides each write by the read, so `doctor` al
|
|
|
876
894
|
divides past the largest number, which would put the restart line at Infinity, where no session reaches it.
|
|
877
895
|
The ledger, `compile` and the advisory hold the same ranges themselves rather than trusting that `doctor` ran.
|
|
878
896
|
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
|
|
897
|
+
stderr what it could not use and reports conditionally with fallback figures; any invalid flag prevents a Stop hold. An invalid resume estimate stays unknown. A threshold that a session's fresh
|
|
880
898
|
context still carries past the largest number is none: the ledger stops with exit 2, and the advisory says it
|
|
881
899
|
is not known. `doctor` refuses the key in a manifest declaring a version before 2.12, gated from birth as
|
|
882
|
-
`sessions` was,
|
|
900
|
+
`sessions` was, `restart` in one before 2.13, and `resume` before 2.16. Doctor and compile check the finite nonnegative resume bound independently. `compile` refuses `restart` there too, with exit 2 and
|
|
883
901
|
nothing written, so that no block is compiled from a declaration its own version's validator refuses.
|
|
902
|
+
For declared `restart`, the compiler requires a string `MAJOR.MINOR` on major 2, minor at least 13;
|
|
903
|
+
missing or malformed versions and other majors stop with exit 2 before artifacts change. The compiler
|
|
904
|
+
still accepts future minors on major 2 for this key; this is not a general manifest-version check or
|
|
905
|
+
a claim that `doctor` accepts those versions.
|
|
884
906
|
`restart` is one of two strings, which the subset types in full, and the ledger and `compile` refuse any other
|
|
885
907
|
with exit 2.
|
|
886
908
|
|
|
@@ -907,14 +929,15 @@ The guidance itself, which is what the host loads.
|
|
|
907
929
|
| Key | What it is |
|
|
908
930
|
|---|---|
|
|
909
931
|
| `tier` | `always`, `on-path`, `on-invoke` or `on-read`: `0036`'s four tiers, spelled as that proposal spells them. |
|
|
910
|
-
| `paths` | On-path only,
|
|
932
|
+
| `paths` | On-path only, as an alternative to `target`: 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. |
|
|
933
|
+
| `target` | On-path only, as an alternative to `paths`: a literal child-directory `CLAUDE.md`, such as `api/CLAUDE.md`. Added within the existing 2.10 unit contract to complete accepted `0036`; older compilers reject this new unit field, so upgrade the compiler before using it. |
|
|
911
934
|
| `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
935
|
|
|
913
936
|
**The tier is declared, never inferred**, and a unit sits in one tier, so what a unit costs is what its
|
|
914
937
|
tier costs and nothing has to read the file to find out where it belongs.
|
|
915
938
|
|
|
916
939
|
**`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
|
|
940
|
+
context; an `on-path` unit is either a rule scoped by `paths:` or the nested instruction file declared by `target`; an `on-invoke` unit is a project skill, whose
|
|
918
941
|
description is always loaded and whose body loads when it is invoked; and an `on-read` unit is one line in
|
|
919
942
|
an unscoped index of pointers, naming its file, its size in whole KB and its description (the size since
|
|
920
943
|
2026-09-24, so a session knows what it passes over), while its file stays where it is for the agent to
|
|
@@ -927,8 +950,54 @@ each rule it wrote: it rewrites or removes only a rule the marker lists, stops w
|
|
|
927
950
|
replace one it does not list, and stops too where the directory holds Markdown files and no marker in its
|
|
928
951
|
form. It writes and removes nothing through a link.
|
|
929
952
|
|
|
953
|
+
**Nested targets use two portable ownership records.** The first-line HTML mark binds the workspace,
|
|
954
|
+
source and canonical target relative to the project; `.claude/rules/portulan/.instructions` pairs that source with its
|
|
955
|
+
target. Both are committed, so clones can regenerate and check the same files. A missing owned file or
|
|
956
|
+
changed body is repairable drift (`--check` exits 1). A human file, missing/torn/foreign mark or registry,
|
|
957
|
+
link, nonregular output or ambiguous case-only move refuses before compilation writes anything.
|
|
958
|
+
Complete atomic writes and exclusive new claims prevent a pending target from replacing a human file
|
|
959
|
+
that arrives after planning. Intermediate registry rows retain old and new ownership until publication
|
|
960
|
+
and stale cleanup finish, so a later run can recover an interrupted move. Only proved-owned files are
|
|
961
|
+
retired when a target, tier or unit changes. Split/join rollback checks post-write bytes and file
|
|
962
|
+
identity, recreates a deleted file exclusively, and preserves later human changes while reporting
|
|
963
|
+
incomplete recovery. Complete prepared temporary files are recognized only by their reserved names
|
|
964
|
+
and validated ownership records or destination-bound marks, and removed on retry. Malformed or
|
|
965
|
+
unproved debris is preserved for manual review. Impossible stable multi-source records are refused.
|
|
966
|
+
When a target is absent after interruption, agreeing proved artifacts retain its observed mode.
|
|
967
|
+
Those witnesses and their source ownership survive until replacement publication, including a
|
|
968
|
+
source rename and another interruption. Disagreeing modes refuse. A new target without an observed
|
|
969
|
+
entry or proved permission witness uses ordinary creation permissions.
|
|
970
|
+
Existing physical path spelling must match the declared target; distinct case-sensitive paths remain
|
|
971
|
+
independent. Owned target sets use one spelling for every shared directory prefix, including pending
|
|
972
|
+
and retired rows. Init projects the atomic writer's parent-directory permissions, not a file-write check.
|
|
973
|
+
Read-only checking requires no mutation permission, and stable nested output
|
|
974
|
+
produces no registry transition. Source slots and compiler control directories are reserved.
|
|
975
|
+
|
|
976
|
+
A target has 1–32 child directories, each at most 128 ASCII characters, starting with a letter or digit,
|
|
977
|
+
then letters, digits, dots, underscores, spaces or hyphens; no trailing dot/space, device names, globs,
|
|
978
|
+
backslashes, traversal or a directory named `CLAUDE.md` (case-insensitive). That last exclusion
|
|
979
|
+
prevents a target from becoming another target's ancestor file. The full path is at most 1,024 characters and ends exactly `CLAUDE.md`.
|
|
980
|
+
This bounded portable subset avoids platform-dependent ownership aliases. Two units cannot claim one
|
|
981
|
+
target, including a spelling differing only in case. `target` and `paths` are mutually exclusive.
|
|
982
|
+
Nested bodies refuse actionable at-sign imports using the shared lexer, including absent, absolute,
|
|
983
|
+
home and target-relative paths, without probing them. Supported code spans and fences remain literal.
|
|
984
|
+
The body therefore cannot acquire a new import by moving beside a different file.
|
|
985
|
+
|
|
986
|
+
The on-path claim assumes Claude starts at the project root. A session started inside the subtree may
|
|
987
|
+
load its ancestor `CLAUDE.md` at startup; an explicit root import can load it eagerly too. Project-root
|
|
988
|
+
budget accounting excludes unimported nested files and includes files explicitly imported by its roots.
|
|
989
|
+
See [Claude's loading contract](https://code.claude.com/docs/en/memory#how-claude-md-files-load).
|
|
990
|
+
Prospective compilation includes pending pre-existing guidance and measures its after corpus when
|
|
991
|
+
representable. Native absolute imports may withhold that optional figure; a successful write measures
|
|
992
|
+
the actual emitted result. An explicitly imported nested body can change that domain.
|
|
993
|
+
Vendor switches recheck observed identities and bytes before retiring material, preserve changed or
|
|
994
|
+
newly arrived files, and never recursively remove the old residence. Their residue advice uses the
|
|
995
|
+
manifest's actual governed tree.
|
|
996
|
+
The nested file's pure body is emitted once; its derived subtree scope is used for hosts' pointer
|
|
997
|
+
fallbacks. The compiler does not emit a second scoped rule for it.
|
|
998
|
+
|
|
930
999
|
**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
|
|
1000
|
+
frontmatter is its consumer's to refuse: an unknown key, a tier outside the four, `paths` or `target` on a unit that is
|
|
932
1001
|
not on-path or reaching outside the repository, a missing `description` or one on an `always` unit, a unit
|
|
933
1002
|
with no guidance under its frontmatter, a link out of the workspace or into a directory `compile` writes into,
|
|
934
1003
|
a slot in one of those (`.claude/`, or the workspace's `compile/`, neither of which `vendor` carries), a name
|
|
@@ -955,6 +1024,73 @@ in it still reads from the repository root, as it did in `CLAUDE.md`.
|
|
|
955
1024
|
puts every section back as it stands, saying which one the digest shows edited since, and refusing a unit
|
|
956
1025
|
re-tiered since, or a file not at the top of this slot, rather than undo its tier or remove it.
|
|
957
1026
|
|
|
1027
|
+
**The CLI split and join can resume an interrupted source migration.** Before claiming a source,
|
|
1028
|
+
`instructions --write` records the exact original operation in the reserved project-root directory
|
|
1029
|
+
`.portulan-instructions-journal` (mode 0700, metadata 0600). A retry with the original split or join
|
|
1030
|
+
command restores held originals and proves the saved operation again with the same planner before
|
|
1031
|
+
continuing. Units precede split roots; join roots precede unit retirement. Exact bytes, modes and
|
|
1032
|
+
recorded identities distinguish the operation's own publications from an independent save, even an
|
|
1033
|
+
identical replacement. A conflict preserves both entries and names the retained journal or claim.
|
|
1034
|
+
The intended whole source set and regenerated guidance must agree before commit is recorded.
|
|
1035
|
+
An unchanged or absent second root remains a dependency of the proof, rechecked before source edits
|
|
1036
|
+
and completion. Initial observations of both roots and original operands must be ordinary singly
|
|
1037
|
+
linked entries before a journal is created; an unsupported alias refuses without leaving a journal.
|
|
1038
|
+
A newly arrived reference cannot authorize retirement of the unit it names. Live references include
|
|
1039
|
+
moved markers and actionable native `@` imports, resolved relative to each root with the import lexer;
|
|
1040
|
+
inline and fenced examples stay literal. Normalized in-project paths and file or directory aliases
|
|
1041
|
+
are compared with direct operands even when the unit is absent. Reference discovery reads the two
|
|
1042
|
+
roots, without traversing imported bodies. Read-only split/join runs the structural source checks
|
|
1043
|
+
before offering a write; join checks both prospective root images for references to units it would
|
|
1044
|
+
retire. Write-time checks still revalidate those inputs. Split, join and upgrade form `0009` validate
|
|
1045
|
+
prospective source edits through the shared isolated
|
|
1046
|
+
guidance view before offering a write. The view applies creations and deletions together, recompiles
|
|
1047
|
+
all guidance and measures the resulting Claude corpus, including generated leads and index imports.
|
|
1048
|
+
Its compiler projection origin preserves engine/adopter identity and maps contained absolute lead
|
|
1049
|
+
and gate dependencies into the view. Aliases retain their directory entries and resolve to the same
|
|
1050
|
+
projected bytes, including dangling links after retirement. The view checks its emitted result again,
|
|
1051
|
+
retains retirements as absence through aliases, including case spellings first seen after emission,
|
|
1052
|
+
and distinguishes separate hard-link entries. Optional native directory aliases use the final file's
|
|
1053
|
+
project membership, including operands spelled outside and routes returning from outside. Their
|
|
1054
|
+
original lexical directory remains the base for later imports; outside bodies are not copied.
|
|
1055
|
+
Optional native non-files and missing imports follow
|
|
1056
|
+
the corpus reader; denied reads and required inputs remain checked. Linked ordinary outputs refuse
|
|
1057
|
+
a migration that would change or retire them; native compilation refuses to rewrite such an output,
|
|
1058
|
+
including a retiring marker's transitional write, before gate or persona publication. It derives
|
|
1059
|
+
the actual workspace, context slot, ownership and packs without reading another host's
|
|
1060
|
+
unrelated root. Unchanged read-only files retain their permissions. A deleted input cannot be copied
|
|
1061
|
+
back from the original tree by a later dependency lookup. Unrepresentable native absolute imports
|
|
1062
|
+
withhold only the optional numerical after forecast; successful writes measure the actual result.
|
|
1063
|
+
Publication snapshots retain their modes through rollback. If a changed root cannot be restored,
|
|
1064
|
+
rollback retains its dependent units and reports the pending operation instead of removing their text.
|
|
1065
|
+
Before unlinking a unit, the journal confirms its acquired public entry and pairs that fact with the
|
|
1066
|
+
exact claim intent, distinguished from preparation aliases. After interruption, a proved retired unit
|
|
1067
|
+
still referenced by either root is recreated exclusively when its path is absent, before the retry
|
|
1068
|
+
refuses an independently edited root. Handled failures restore the current attempt's safe images;
|
|
1069
|
+
the journal remains until the whole original operation has reached a proved terminal state.
|
|
1070
|
+
The saved proof contains only the two root observations and directly involved units; unrelated
|
|
1071
|
+
context bodies used to forecast index growth are not copied into the bounded journal.
|
|
1072
|
+
|
|
1073
|
+
This is a local process-interruption protocol, demonstrated with SIGKILL, rather than
|
|
1074
|
+
a power-loss durability guarantee. Exclusive immutable lease children permit one live writer or
|
|
1075
|
+
recoverer in the same verified boot/PID namespace; a live, inaccessible or reused PID prevents
|
|
1076
|
+
takeover. Another namespace refuses. A cut after a source claim converges automatically. Unproved
|
|
1077
|
+
partial metadata, an unrecorded preparation file (including a retry killed before its first durable
|
|
1078
|
+
record), an ownerless initializer or an empty directory left after final cleanup can require
|
|
1079
|
+
manual review of the exact reported file or directory: preserve any source or claim bodies, then remove only
|
|
1080
|
+
the reviewed residue and rerun the original command. At initialization the source set is original;
|
|
1081
|
+
at final cleanup it is fully committed. Do not delete a pending journal merely to bypass its refusal.
|
|
1082
|
+
|
|
1083
|
+
Read-only `instructions` reports a pending migration without changing it. Guidance compilation/check
|
|
1084
|
+
refuses a partial source set; a complete source set may still report stale generated guidance until
|
|
1085
|
+
the retry compiles it. `init`, `vendor` and `upgrade` refuse a pending journal; vendor also refuses
|
|
1086
|
+
instruction claim/staging residue rather than copying complete source bodies into another residence.
|
|
1087
|
+
The journal cannot overlap a current declared slot, checked again during recovery and terminal cleanup.
|
|
1088
|
+
Vendor refuses reserved journal paths found in its inventory and rechecks pending operations before
|
|
1089
|
+
publication and governance retirement. This recovery promise covers the CLI split/join only;
|
|
1090
|
+
upgrade form `0009` shares the source plan and guarded writes but has no process-restart journal.
|
|
1091
|
+
The distinction keeps the recovery record confined to this operation instead of creating a general
|
|
1092
|
+
workspace transaction format. Provenance: #629's independent source-interruption review.
|
|
1093
|
+
|
|
958
1094
|
**An `always` unit may import whole files and write out another file's lead sentences or a gate policy's
|
|
959
1095
|
gates**, so a card carries a fact without holding a second copy of it:
|
|
960
1096
|
|
|
@@ -971,6 +1107,15 @@ gates**, so a card carries a fact without holding a second copy of it:
|
|
|
971
1107
|
a path-scoped rule's included. The vendored `AGENTS.md` carries each import as a pointer line to the
|
|
972
1108
|
file in the vendored tree, and one for each file that file imports in turn, to the depth the host would
|
|
973
1109
|
load (2026-09-24), and `vendor` refuses an import leaving the workspace, which is all that tree holds.
|
|
1110
|
+
**The shared reader refuses unsupported import-bearing regions by name**
|
|
1111
|
+
([#471](https://github.com/sleepy-panda-srl/portulan/issues/471)): inline emphasis,
|
|
1112
|
+
escape or tag adjacency; code spans on comment-opening lines or across lines;
|
|
1113
|
+
HTML blocks, indented code and indented fences; blockquoted lists; and list
|
|
1114
|
+
continuations after nested lists, tags, indented code or HTML blocks. Refusal
|
|
1115
|
+
replaces a guessed import set, including in nested files and instruction
|
|
1116
|
+
rewriting. Syntax without an import and unrelated imports outside those regions
|
|
1117
|
+
retain their supported behavior. This is bounded validation, not general
|
|
1118
|
+
CommonMark or live-host lexer parity.
|
|
974
1119
|
- **A line `<!-- leads: <path> -->`, alone on its line, is replaced by the first sentence of each item in
|
|
975
1120
|
the first list of the file it names**, each item opening with a bold lead. The file stays the one
|
|
976
1121
|
source of those sentences, so a change there is drift until the unit is recompiled. `compile` refuses
|