@sleepy_panda_srl/portulan 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +7 -6
  2. package/cli/README.md +142 -291
  3. package/cli/advisory.mjs +354 -0
  4. package/cli/comments.mjs +932 -0
  5. package/cli/compile.mjs +1443 -1210
  6. package/cli/context.mjs +883 -0
  7. package/cli/control-chars.mjs +12 -301
  8. package/cli/discover.mjs +17 -574
  9. package/cli/doctor.mjs +225 -1071
  10. package/cli/drills.mjs +1107 -0
  11. package/cli/feedback.mjs +16 -237
  12. package/cli/finish.mjs +437 -0
  13. package/cli/form.mjs +644 -0
  14. package/cli/fuzz-shell.mjs +595 -0
  15. package/cli/gate.mjs +7 -198
  16. package/cli/goldens.mjs +311 -0
  17. package/cli/index.mjs +305 -655
  18. package/cli/init.mjs +244 -564
  19. package/cli/inside.mjs +3 -28
  20. package/cli/instructions.mjs +829 -0
  21. package/cli/ledger.mjs +778 -0
  22. package/cli/librarian.mjs +135 -599
  23. package/cli/manifest.mjs +2 -39
  24. package/cli/mutants.mjs +764 -0
  25. package/cli/new.mjs +12 -243
  26. package/cli/pack-identity.mjs +6 -68
  27. package/cli/pack-version.mjs +13 -248
  28. package/cli/plugin-lint.mjs +42 -658
  29. package/cli/portulan.mjs +5 -124
  30. package/cli/recipe-set.mjs +16 -240
  31. package/cli/release-eval.mjs +748 -0
  32. package/cli/review-meter.mjs +500 -0
  33. package/cli/rule-carriers.mjs +12 -175
  34. package/cli/sessions.mjs +82 -0
  35. package/cli/skill-goldens.mjs +424 -0
  36. package/cli/skills-set.mjs +12 -414
  37. package/cli/stop-gate.mjs +126 -454
  38. package/cli/symbols.mjs +1132 -0
  39. package/cli/telemetry.mjs +582 -0
  40. package/cli/upgrade.mjs +171 -314
  41. package/cli/vendor.mjs +146 -476
  42. package/cli/version-carriers.mjs +9 -74
  43. package/core/README.md +1 -1
  44. package/core/engine.md +3 -9
  45. package/core/operating/autonomy.md +1 -1
  46. package/core/operating/context.md +183 -0
  47. package/core/operating/evolution.md +3 -3
  48. package/core/operating/loop.md +13 -20
  49. package/core/operating/memory.md +12 -3
  50. package/core/operating/sessions.md +100 -0
  51. package/core/personas/librarian.md +2 -2
  52. package/core/skills/consolidate/SKILL.md +4 -2
  53. package/core/templates/gate-policy.md +27 -8
  54. package/core/templates/handoff.md +28 -15
  55. package/core/templates/memory-entry.md +1 -0
  56. package/evals/README.md +320 -0
  57. package/evals/releases/0.1.3.json +134 -0
  58. package/evals/releases/0.1.3.md +64 -0
  59. package/evals/releases/0.2.0.json +146 -0
  60. package/evals/releases/0.2.0.md +67 -0
  61. package/evals/releases/README.md +32 -0
  62. package/package.json +9 -2
  63. package/packs/rituals/checkpoints/README.md +9 -2
  64. package/packs/rituals/checkpoints/pack.json +1 -1
  65. package/packs/rituals/checkpoints/skills/pre-commit/SKILL.md +32 -2
  66. package/packs/tools/github/verify/actions-pinned.sh +10 -55
  67. package/spec/README.md +112 -6
  68. package/spec/migrations/0001-repository-declares-its-tree.mjs +1 -26
  69. package/spec/migrations/0002-bundle-fallback-path.mjs +13 -74
  70. package/spec/migrations/0003-handoff-index-not-kept.mjs +66 -0
  71. package/spec/migrations/0004-changelog-fragments.mjs +53 -0
  72. package/spec/migrations/0005-session-log-retired.mjs +69 -0
  73. package/spec/migrations/0006-boot-card.mjs +77 -0
  74. package/spec/migrations/0007-guidance-compiled.mjs +31 -0
  75. package/spec/migrations/0008-card-reading.mjs +55 -0
  76. package/spec/migrations/0009-instruction-sections-on-read.mjs +60 -0
  77. package/spec/migrations/0010-comment-rail.mjs +57 -0
  78. package/spec/migrations/README.md +45 -10
  79. package/spec/pack.schema.json +1 -1
  80. package/spec/slots.md +359 -23
  81. package/spec/workspace.schema.json +145 -4
@@ -1,50 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // Portulan — every prose statement of the CURRENT version agrees with `package.json`.
3
- //
4
- // ## The defect this exists for
5
- //
6
- // `README.md` and `.portulan/products/portulan/product.md` both said the newest release entry was
7
- // `0.2.0` while `package.json`, both plugin manifests and the registry said `0.1.0`. They were fixed
8
- // together — and then, one release later, the SAME PAIR drifted again: `README.md` still read
9
- // "Current release: `0.1.0`" inside the change that cut `0.1.1`. Twice, with the pairing already
10
- // written down in `docs/plan.md` both times.
11
- //
12
- // **A sibling list in prose is not a rail.** This is the rail.
13
- //
14
- // It matters more than a tidy number because `README.md` is in `package.json`'s `files`, and npm
15
- // FREEZES a README per published version: a wrong current-version sentence that reaches a publish
16
- // cannot be corrected by any edit to `main`, only by cutting another release. That is exactly what
17
- // `0.1.1` was for.
18
- //
19
- // ## Why the record layer is excluded, which is the whole design
20
- //
21
- // This repository writes about its own defects, so the retired sentences are QUOTED in the files that
22
- // record fixing them: `CHANGELOG.md` carries *"The newest release entry is `0.2.0`"* inside the entry
23
- // explaining that it was retired, and two handoffs plus `docs/plan.md` do the same. A matcher over
24
- // everything reds on the repository's own account of the repair — the failure mode #286 measured,
25
- // where both arms it proposed died on this repository's own review corpus.
26
- //
27
- // So the scan is scoped to LIVE prose and the record layer is excluded BY PATH. The exclusion is not
28
- // a convenience: a dated record is supposed to preserve a sentence that is no longer true, and a rail
29
- // that forbids that would forbid the record.
30
- //
31
- // ## What it cannot do, stated rather than discovered
32
- //
33
- // A current-version claim spelled a NEW way, in any file, is invisible to it. There is no way to tell
34
- // "this prose asserts the current version" from "this prose mentions a version" without a matcher, and
35
- // a matcher only knows the spellings it was given. The accepted practice for that residual is #286's,
36
- // ruled by the maintainer: **a new spelling gets a pattern here and a fixture first.**
37
- //
38
- // The half that IS closed beyond drift: every file in MUST_CARRY has to produce at least one match, so
39
- // deleting or rewording a known carrier reds instead of silently shrinking the rail's reach.
40
- //
41
- // Three further residuals, named because an unstated limit is the one a reader assumes away:
42
- // · A live claim placed UNDER an excluded prefix is invisible. `.portulan/memory/` in particular
43
- // holds doctrine-shaped notes rather than only dated records, so a current-version sentence
44
- // written there would not be checked.
45
- // · An UNTRACKED carrier is invisible: enumeration is `git ls-files`, deliberately, because a rail
46
- // that reads the working tree grades files no commit will ever contain.
47
- // · A NON-MARKDOWN carrier is invisible: the pathspec is `*.md`.
2
+ // Every prose statement of the current version agrees with `package.json`.
48
3
  //
49
4
  // Exit 0 green · 1 a finding · 2 could not run.
50
5
 
@@ -53,23 +8,22 @@ import { join } from "node:path";
53
8
  import { pathToFileURL } from "node:url";
54
9
  import { execFileSync } from "node:child_process";
55
10
 
56
- /** Raised when a precondition fails. Always exit 2 — never a finding about the prose. */
57
11
  export class CouldNotRun extends Error {}
58
12
 
59
- // The spellings that assert a current version. Each must capture the version in group 1.
13
+ // Each captures the version in group 1; a claim spelled any other way goes unseen until it gets a pattern here.
60
14
  export const PATTERNS = [
61
15
  { id: "current-release", re: /\*\*Current release:\s*`([^`]+)`\*\*/g },
62
16
  { id: "newest-entry", re: /newest release entry is\s*`([^`]+)`/g },
63
17
  { id: "supported-current", re: /\|\s*`([^`]+)`\s*\|\s*Yes\s*—\s*the current release\s*\|/g },
64
18
  ];
65
19
 
66
- // Files that MUST carry at least one claim. Losing one is a finding: it means a carrier was deleted
67
- // or reworded, which is how this rail would otherwise quietly stop covering what it was built for.
68
20
  export const MUST_CARRY = ["README.md", "SECURITY.md", ".portulan/products/portulan/product.md"];
69
21
 
70
22
  // The record layer. These files preserve sentences that are no longer true, on purpose.
71
23
  export const RECORD_PREFIXES = [
72
24
  "CHANGELOG.md",
25
+ "changes/",
26
+ "evals/releases/",
73
27
  "docs/plan.md",
74
28
  "docs/milestones/",
75
29
  ".portulan/handoffs/",
@@ -80,7 +34,6 @@ export const RECORD_PREFIXES = [
80
34
 
81
35
  export const isRecord = (p) => RECORD_PREFIXES.some((r) => (r.endsWith("/") ? p.startsWith(r) : p === r));
82
36
 
83
- /** Every tracked Markdown file that is not part of the record layer. Derived, never a written list. */
84
37
  export function liveProseFiles(root) {
85
38
  let out;
86
39
  try {
@@ -96,11 +49,7 @@ export function liveProseFiles(root) {
96
49
  export function declaredVersion(root) {
97
50
  let raw;
98
51
  try {
99
- // From the INDEX, like the prose it is compared against. Reading the version from the
100
- // worktree while reading carriers from the index compares two different trees: a staged
101
- // version bump with an unchanged worktree would grade staged prose against the OLD version,
102
- // reporting drift that is not there — or, reversed, greening drift that is. Half a fix is
103
- // how this rail's own index change first shipped.
52
+ // From the index, like the prose: a version read from the worktree would grade staged prose against another tree.
104
53
  raw = execFileSync("git", ["-C", root, "show", ":package.json"], { encoding: "utf8" });
105
54
  } catch (e) {
106
55
  throw new CouldNotRun(`could not read package.json from the index: ${e.message}`);
@@ -115,7 +64,6 @@ export function declaredVersion(root) {
115
64
  return v;
116
65
  }
117
66
 
118
- /** Every current-version claim in one file's text, with the line it sits on. */
119
67
  export function claimsIn(text) {
120
68
  const found = [];
121
69
  for (const { id, re } of PATTERNS) {
@@ -139,15 +87,10 @@ export function inspect(root) {
139
87
  for (const rel of files) {
140
88
  let text;
141
89
  try {
142
- // The INDEX, not the working tree — `pack-identity` reads the same way and for the same
143
- // reason. Enumerating from the index and then reading the worktree lets a staged drift
144
- // with a reverted worktree copy report green while the commit ships the drift.
90
+ // The index, not the worktree: a reverted worktree copy would hide a staged drift the commit ships.
145
91
  text = execFileSync("git", ["-C", root, "show", `:${rel}`], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
146
92
  } catch (e) {
147
- // NOT `continue`. `git ls-files` just named this path, so failing to read its blob means
148
- // the rail could not examine live prose it is responsible for — and skipping it would
149
- // report GREEN over a file never looked at. That is this session's defect class exactly,
150
- // and it was sitting inside the rail built against it. Could-not-run, never a green.
93
+ // Not `continue`: skipping a file git just listed would report green over prose never read.
151
94
  throw new CouldNotRun(`could not read ${rel} from the index: ${e.message}`);
152
95
  }
153
96
  for (const c of claimsIn(text)) {
@@ -187,12 +130,7 @@ export function main(argv, stdout = process.stdout, stderr = process.stderr) {
187
130
  return 0;
188
131
  }
189
132
 
190
- // The entry guard, in the ONE form `cli/rule-carriers.mjs` designates after this repository got it
191
- // wrong twice. `file://${argv[1]}` is NOT that form: `import.meta.url` percent-encodes, this working
192
- // copy lives under a path with spaces, and the comparison failed — so the tool exited 0 having run
193
- // nothing. **A green that is the tool never starting**, shipped inside the rail built against exactly
194
- // that. The realpath fallback covers the symlink case an npm `bin` produces, in a `try` because a
195
- // missing path must answer no rather than throw.
133
+ // URLs on both sides, since `import.meta.url` percent-encodes; the realpath covers a symlinked npm `bin`.
196
134
  function isMain() {
197
135
  const invoked = process.argv[1];
198
136
  if (!invoked) return false;
@@ -204,8 +142,5 @@ function isMain() {
204
142
  }
205
143
  }
206
144
 
207
- // `process.exitCode` rather than `process.exit`, which `cli/control-chars.mjs:540` settled for this
208
- // repository: exiting outright can truncate a pipe that has not drained. It matters more here than
209
- // stylistically — a truncated `ok` line IS exit 0 with no output, which is the precise shape of the
210
- // false green this file's entry guard was fixed for. The same defect, one door along.
145
+ // `exitCode`, not `exit()`, so a pipe that has not drained is not cut short.
211
146
  if (isMain()) process.exitCode = main(process.argv.slice(2));
package/core/README.md CHANGED
@@ -13,7 +13,7 @@ loads on demand.
13
13
  | Path | What lives here |
14
14
  |---|---|
15
15
  | [`engine.md`](engine.md) | The kernel: cascade, loop, non-negotiable gates, and the map. Always loaded. |
16
- | [`operating/`](operating/) | The doctrine, one concern per file: loop · autonomy · verification · memory · evolution · safety. |
16
+ | [`operating/`](operating/) | The doctrine, one concern per file: loop · autonomy · verification · memory · evolution · safety · context · sessions. |
17
17
  | [`templates/`](templates/) | The artifacts the doctrine references: repo-card · task · handoff · proposal · memory-entry. |
18
18
  | [`personas/`](personas/) | Roles an agent takes, as context firewalls — each with a `tools:` allow-list. |
19
19
  | [`skills/`](skills/) | Progressive-disclosure procedures in `SKILL.md` form. |
package/core/engine.md CHANGED
@@ -1,12 +1,7 @@
1
1
  # Portulan engine
2
2
 
3
3
  > The always-loaded kernel: the invariants every agent carries, and the map to the doctrine behind
4
- > them. Kept small on purpose — everything here is load-bearing; everything else loads on demand. This
5
- > file is the core's source contribution to the always-loaded layer; `portulan vendor --host` inlines it
6
- > verbatim into a vendored `AGENTS.md` beside the workspace's own layer, for a host that cannot install
7
- > the plugin. The **pack** layer is named in that file rather than composed into it — a pack resolves
8
- > from a feed at a pinned version and **vendoring resolves nothing**, which is unchanged. Pack-cache
9
- > discovery landed at milestone 7: the CLI reads the host's record, a vendored file cannot.
4
+ > them. Kept small on purpose — everything here is load-bearing; everything else loads on demand.
10
5
 
11
6
  ## Resolution cascade
12
7
 
@@ -38,10 +33,9 @@ scale down. → `operating/loop.md`
38
33
 
39
34
  ## The map
40
35
 
41
- - `operating/` — the doctrine: loop · autonomy · verification · memory · evolution · safety.
36
+ - `operating/` — the doctrine: loop · autonomy · verification · memory · evolution · safety · context · sessions.
42
37
  - `templates/` — the artifacts: repo-card · task · handoff · proposal · memory-entry.
43
38
  - `personas/` — roles as context firewalls, each with a `tools:` allow-list.
44
39
  - `skills/` — progressive-disclosure procedures; a skill must enforce, measure, or earn its tokens.
45
40
 
46
- Every rule here carries its rationale and provenance in the doc it links to. Nothing in this kernel is
47
- prose for its own sake: when a line stops being load-bearing, it moves out of the kernel.
41
+ Every rule here carries its rationale and provenance in the doc it links to.
@@ -54,7 +54,7 @@ else safe.)_
54
54
  by changing the rule itself through the evolution gate — never by runtime approval.
55
55
 
56
56
  That distinction is the whole reason the fourth tier exists rather than being folded into Gated, and it
57
- is not academic: a compiler with three tiers must file *"no agent edits the constitution"* under Gated,
57
+ is not academic: a compiler with three tiers must file *"no session certifies its own checkpoint"* under Gated,
58
58
  and Gated compiles to a prompt. *Never* would become *unless somebody clicks yes* — a difference no
59
59
  reader of the gate map would see, produced entirely by the vocabulary being one word short. _(Found by
60
60
  building the compiler: milestone 4 needed a fourth class before it had a schema, and the workspace layer
@@ -0,0 +1,183 @@
1
+ # Context
2
+
3
+ > Core doctrine — loaded on demand, which on this page's own terms is the on-read tier. What a host
4
+ > loads into every context is paid for again by every fresh context: each session, subagent, checkpoint
5
+ > and persona. So guidance sits in load tiers, the always tier is budgeted like memory, and a demotion
6
+ > is measured before and after. `loop.md` manages the window while a task runs; this page governs what
7
+ > the host loads into it, and when. _(Proposal
8
+ > [`0036`](../../.portulan/proposals/0036-what-a-host-loads-into-every-context-is-budgeted.md).)_
9
+
10
+ ## Four load tiers
11
+
12
+ **Every piece of guidance sits in a load tier, and belongs in the latest tier that still reaches the
13
+ agent in time.** There are four:
14
+
15
+ - **Always**: loaded into every context, which covers project instruction files and their imports,
16
+ unscoped rules, and skill and agent descriptions.
17
+ - **On-path**: loaded when the agent first touches a matching path, which covers nested instruction files
18
+ and path-scoped rules.
19
+ - **On-invoke**: loaded when a skill, persona or ritual runs.
20
+ - **On-read**: loaded when the agent opens the file.
21
+
22
+ The always tier is paid for by every context, whether or not its task needs what the tier holds; a later
23
+ tier is paid for only by the contexts that reach it. *In time* is the limit: guidance that arrives after
24
+ the moment it applies has not reached the agent.
25
+
26
+ A workspace sets its own guidance in these tiers in `slots.context`, since Workspace Definition 2.10: one
27
+ unit per file, each declaring its tier, and `compile` emits each unit in its host's form of that tier.
28
+ _(See [`../../spec/slots.md`](../../spec/slots.md).)_
29
+
30
+ ## One level of index
31
+
32
+ **One level of index only**: an always-tier line may point at content, and a pointer may not point at
33
+ another pointer. A **pointer** is a line the agent reads to decide whether to open something; a file a
34
+ procedure always reads is content loaded in that procedure's tier, not a pointer. So the kernel's map,
35
+ whose lines point at directories of content, is one level, and so is the boot's path to a memory record:
36
+ the boot always reads the memory index, and each index line is the one pointer to its record.
37
+
38
+ _(Motivated, not established, by arXiv 2607.17598, *Is Progressive Disclosure All You Need for
39
+ Long-Context Agents?* (He, Zhao, Wang and Chen, 2026), whose abstract finds that a second, deeper routing
40
+ level never helps and sometimes breaks accuracy. It measured long-document question answering, not
41
+ instruction files.)_
42
+
43
+ ## The always tier is budgeted in tokens
44
+
45
+ **The budget is declared in the manifest and never defaulted**, as `context.always.budget.tokens`. A
46
+ breach is repaired by **demotion** to a later tier, by **merge** or by **retirement**, and never by
47
+ raising the budget in the change that breached it. The shape is memory's on purpose, its limit included:
48
+ a budget is a rail, not an aim, and the no-raise half is a rule no checker establishes, for the reason
49
+ [`memory.md`](memory.md) gives.
50
+
51
+ **Bytes per token are declared too**, as `context.ratio`: measured exactly on demand, by asking the host
52
+ for the true count, and recorded with the host whose exact count calibrated it. The exact measurement
53
+ never runs inside a recipe, because no verify recipe may make a network call, and a ratio that moved
54
+ between runs would make one tree red on one run and green on the next. The keys are argued in
55
+ [`../../spec/slots.md`](../../spec/slots.md).
56
+
57
+ **A report by default, a rail by declaration.** A workspace is failed only against a budget it declared.
58
+
59
+ ## Portulan budgets its own contribution
60
+
61
+ **Portulan budgets its own contribution by default, because it owns it.** That is the kernel, the boot
62
+ skill and the engine half of the boot read-set, skill and persona prompts, hook-injected text, and the
63
+ vendored `AGENTS.md` skeleton. The default is a rail in this repository, not a budget in any adopter's
64
+ manifest, so *never defaulted* holds. This is what lets every install benefit on upgrade without
65
+ configuring anything.
66
+
67
+ **The kernel, [`../engine.md`](../engine.md), is the core's source contribution to the always-loaded
68
+ layer**; `portulan vendor --host` inlines it verbatim into a vendored `AGENTS.md` beside the workspace's
69
+ own layer, for a host that cannot install the plugin. The **pack** layer is named in that file rather
70
+ than composed into it — a pack resolves from a feed at a pinned version and **vendoring resolves
71
+ nothing**, which is unchanged. Pack-cache discovery landed at milestone 7: the CLI reads the host's
72
+ record, a vendored file cannot. Nothing in this kernel is prose for its own sake: when a line stops being
73
+ load-bearing, it moves out of the kernel.
74
+
75
+ ## A fresh context is priced where it is created
76
+
77
+ Whatever spawns one (a checkpoint, a persona, a ritual) states what it loads, and the compiled agent
78
+ carries that. **Reviewers keep the conventions they grade against**: the remedy for an expensive always
79
+ tier is a smaller always tier, never a blind reviewer.
80
+
81
+ ## No optimisation may lower context or code quality
82
+
83
+ Prefer moving content to a later tier over deleting it, keep one source per fact, and measure before and
84
+ after. **A demotion states both figures.**
85
+
86
+ **What this does not ask for**: a defaulted workspace rail, lossy compression, a codebase vector index,
87
+ switching model or effort mid-session, output-shortening tricks, or any change to a host. Portulan
88
+ compiles to what hosts load and does not change how they load it.
89
+
90
+ ## Every request pays for what the session has read
91
+
92
+ Each tool call ends a request, and the next request sends the whole context again: the host's own prefix,
93
+ the always tier, and everything read and printed so far. The prompt cache makes the repeat cheaper, not
94
+ free. In Claude Code 2.1.281 the main conversation's cache lives an hour on a subscription within its
95
+ usage limits and five minutes on an API key or a cloud provider, a subagent's five minutes unless set
96
+ (its settings `promptCacheTtl` and `subagentPromptCacheTtl`, read in its program text on 2026-09-24).
97
+ So what a session adds, it pays for on every request after, and a task done in fewer requests re-sends
98
+ less. `compile` writes the lead of each rule below onto every boot card, Portulan's and each consumer's:
99
+
100
+ - **Send independent tool calls in one request.** Reads, searches and commands whose inputs do not wait
101
+ on each other's output cost one request together and one each apart, and each request re-sends the
102
+ context.
103
+ - **Read a file once; reuse what your context holds.** A second read of an unchanged file adds a second
104
+ copy, paid for on every later request. After an Edit, do not read the file back to check it: the host
105
+ refuses an edit it cannot apply, and says so.
106
+ - **Open a file only when the task needs it, and one of 300 lines or more only where it does:
107
+ `node <plugin root>/cli/symbols.mjs <file>` outlines it, and `<file>#<heading>` prints one section.**
108
+ The outline gives each part's lines, and a Markdown heading's anchor and size; it costs a line a part
109
+ where a doctrine page or an instruction file runs to thousands of tokens and a task needs one section.
110
+ A link to a heading names its section, so `<file>#<anchor>` reads what the link points at without an
111
+ outline. A shorter file the task changes is read whole, as below. Read a section you will edit with the
112
+ Read tool's `offset` and `limit`, which an Edit may follow. `<plugin root>` is where Portulan is
113
+ installed, the directory whose `core/engine.md` the boot reads; in Portulan's own repository it is the
114
+ repository, and `compile` writes the command as run from there.
115
+ - **Ask for only the output you need: a range, a count, the failures.** Output enters the context as a
116
+ read does. A passing run needs its summary and a failing one the failing check and the fact to act on,
117
+ as [`loop.md`](loop.md)'s compacted error does; `git diff --stat`, `grep -c` and a line range ask for
118
+ less than a whole log, file or tree.
119
+ - **The prompt cache lasts five minutes or an hour: a longer pause, or a fresh subagent where one could
120
+ be resumed, writes the whole context again.** A subagent's context is its own and starts with the
121
+ host's prefix, so start one only where the reads it keeps out of this context cost more than that
122
+ prefix, and resume one rather than start another; [`sessions.md`](sessions.md) holds both, and the
123
+ lifetime a workspace declares. The cost table in the same program text prices a cache write above
124
+ sending the tokens fresh and a cache read below it.
125
+ - **Code should speak for itself: a comment is paid for on every read of its file, so add one only when
126
+ truly needed, concise, meaningful and valuable (less is more), and keep a change's history in its
127
+ commit message.** Claude Code 2.1.281's system prompt tells a model to match the surrounding code's
128
+ comment density, so what a file carries, the next change copies. Truly needed is what no name or
129
+ structure can show, in a line: a host's behaviour at its version, an ordering or a safety constraint the
130
+ code does not make obvious, a limit of the algorithm. A comment never carries a date, a proposal or
131
+ milestone number, a review round or a reviewer's credit, a pull request or issue number, or a narration
132
+ of what the code does; where the code needs one to be understood, rename or extract instead.
133
+ `node <plugin root>/cli/comments.mjs` counts the comment lines that record a change's history, in code
134
+ and in a Markdown file's HTML comments other than the lines a tool reads, and a workspace's `comments`
135
+ recipe holds that count at a limit that only falls.
136
+
137
+ ## Code is read by symbol
138
+
139
+ A session outlines a code file of 300 lines or more before it opens it, then reads the spans it needs: in
140
+ Claude Code, the Read tool with `offset` at a span's first line and `limit` its length. **A file under 300
141
+ lines that the task changes is read whole**, since most of it is context for the change and the saving is
142
+ smallest there, and so is any file whose outline cannot say where a change goes. A span read the host does
143
+ not cut is a full read of those lines, so an Edit may follow it: in Claude Code 2.1.281 a Read is marked
144
+ partial, and an Edit or a Write after it refused, only when the Read's token cap, 25,000 by default, cut it
145
+ (read in its program text on 2026-09-24). [`cli/symbols.mjs`](../../cli/symbols.mjs) prints the outline,
146
+ one line per symbol, and `--find <name>` locates a definition; it outlines Markdown by its headings, with
147
+ each section's anchor and size, the same way. It prints from the file as it is, so no map is committed to
148
+ go stale. Nothing checks that a session outlined first; what the rule saves is measured by rerunning a
149
+ task, never by a recipe, which may not read the host's usage records.
150
+
151
+ ## What is machinery today, and what is not
152
+
153
+ As of Workspace Definition 2.9 a workspace can declare the budget and the ratio, and both are checked
154
+ for shape, by the schema and by `doctor`; the manifest key refuses a budget with no ratio to count it by.
155
+ **The always tier is measured and reported.** [`cli/context.mjs`](../../cli/context.mjs) measures what
156
+ Claude Code loads into every context from a workspace's `tree`; `doctor` reports that for every workspace
157
+ and fails it only over a declared budget or where a declared budget cannot be judged; the boot closes
158
+ with the same line; and this repository's `context` recipe rails Portulan's own footprint. **A boot can
159
+ read a card instead of the slots**: the `boot` unit of `slots.context` (Workspace Definition 2.10) is
160
+ compiled into every context, imports whole the files it needs, writes out the lead sentences of others,
161
+ and names the file behind each line, so the rest loads when a rule's path or the card sends a session to
162
+ it. **A team's own instruction file can be split the same way, section by section**: a line
163
+ `<!-- portulan: on-read -->` under a heading of `CLAUDE.md` or `.claude/CLAUDE.md` is the team's word that
164
+ the section may leave every context, and `portulan upgrade` moves it, byte for byte and proved to
165
+ reassemble, into an on-read unit whose index line names the heading and its size in whole KB. `doctor`, the
166
+ boot and `init` offer it where the file is over `0036`'s 8,000-token offer floor, and `doctor` and the boot
167
+ wherever a declared budget is breached, naming there the command that splits while `upgrade` will not run;
168
+ a section is never moved for its size alone. Claude Code 2.1.281 drops a comment standing on its own lines
169
+ before it loads an instruction file or rule (read in its program text on 2026-09-24), so the mark and the
170
+ marker left behind cost nothing. This repository's own card holds its boot under the 7,500-token line of
171
+ 2026-09-23, railed at today's figure plus 2% by the same recipe. A pointer's repository is not measured,
172
+ because a pointer declares no tree. **Still to land**: the exact mode that
173
+ calibrates a ratio, the degradation report that says which tiers a host can express, the Stop-gate's
174
+ named demotion, persona frontmatter and the librarian pass, one change at a time, in `0036`'s order of work
175
+ and under row 12 of [`../../docs/plan.md`](../../docs/plan.md).
176
+ Until each lands, the rule it would enforce is held by the human gate, and saying so is the point: a rule
177
+ nothing checks, presented as one that something does, is the failure this page exists to prevent.
178
+
179
+ _(Provenance: proposal `0036`'s sealed incident, in which an adopting repository's always-loaded
180
+ instruction files measured 130k tokens per fresh context, cache writes were most of the spend, and every
181
+ subagent and checkpoint paid it again, because nothing measured, budgeted or demoted what the host
182
+ loads. Host facts from the Claude Code documentation on costs, prompt caching and memory, checked
183
+ 2026-09-21.)_
@@ -67,9 +67,9 @@ rather than a story about this repository. **Work is agent-drafted and fresh-con
67
67
  moments**: the plan before building starts, the diff before it goes outward, and the exit criteria at
68
68
  the close — where done is *demonstrated, not asserted* (`verification.md`). Two conditions hold the
69
69
  cycle up and neither is optional. The unit of work carries **hard exit criteria** fixed before it
70
- begins, so the close has a standard to grade rather than an impression. And **every session ends by
71
- writing the record** (`loop.md`), so the context arriving at the next moment inherits the reasoning
72
- instead of re-deriving it — a graded moment whose grader must first reconstruct what was decided is
70
+ begins, so the close has a standard to grade rather than an impression. And **every change writes its
71
+ record** (`loop.md`), its why in the commit and any open work in a handoff, so the context arriving at
72
+ the next moment inherits the reasoning instead of re-deriving it — a graded moment whose grader must first reconstruct what was decided is
73
73
  grading its own reconstruction.
74
74
 
75
75
  The altitudes stay separate, and the separation is the part that travels. Core states that the three
@@ -26,26 +26,19 @@ The loop runs inside a finite attention window, so context is managed, not hoard
26
26
  next action — and drop the transcript. The loop is a stateless reducer over that state, so it resumes
27
27
  from the compaction without losing the thread. _(Provenance: HumanLayer ACE-FCA — the loop and
28
28
  compaction; 12-Factor Agents — stateless-reducer resumability.)_
29
- - **Handoffs record decisions and their why**, not just what changed, so the next agent — or the next
30
- window — inherits the reasoning. **Every session ends with a dated handoff** — a *session* being one
31
- bounded working stretch, however the host names it, from the moment work opens to the moment it closes.
32
- Short is fine, absent is not, and an exception is a last resort. The rule is binary on purpose: a
33
- discretionary one cannot be enforced, because no gate can judge whether skipping was warranted, and the
34
- costs are asymmetric — an unnecessary handoff costs five lines, a skipped one loses the reasoning
35
- permanently. Ceremony scales down *inside* the artifact, never by omitting it, so a gate on this checks
36
- that one exists and is dated — never its structure or its length. **Dated** means the filename leads
37
- with an ISO date: `YYYY-MM-DD-{slug}.md`. Naming the form matters, because a rule justified by being
38
- checkable has to say what the checker looks at; a date buried in prose would need parsing and would be
39
- read differently by every writer. It also makes the series sort chronologically for free. An unbroken
40
- series is what makes the record machine-readable: the session-end gate arrived in milestone 4 — it
41
- checks that a handoff dated today exists, never its length or its shape — and milestone 5 built the
42
- librarian that **reads** the series, in three ways that only an unbroken one allows. It generates an
43
- index over it, every field derived. It ages it from git. And it mines it: an incident nothing in the
44
- curated layer points back at is a candidate for the `codify` skill, because a rule whose incident
45
- cannot be traced can never be retired on evidence. The pass also **writes** to the series — a
46
- scheduled pass is a session, so it ends with a dated handoff like any other — which is why nothing
47
- regenerates that index until the pass's own record is on disk. _(Provenance: Cognition. See
48
- `../templates/handoff.md`.)_
29
+ - **A change's why goes in its commit; a handoff carries only the work still open.** A commit's subject
30
+ says what changed and its body says why, in a few lines: the reasoning the next agent cannot
31
+ reconstruct from the diff, kept beside the diff. A session (one bounded working stretch, however the
32
+ host names it) that ends with work not committed and pushed ends with a dated handoff: where things
33
+ stand, the open questions and the single next action, and nothing the diff or a commit already says.
34
+ **Dated** means the filename leads with an ISO date, `YYYY-MM-DD-{slug}.md`, because a rule justified by
35
+ being checkable has to say what the checker looks at, and the series then sorts chronologically for
36
+ free. The session-end gate (milestone 4) checks exactly that: while the tree holds uncommitted or
37
+ unpushed work, a handoff dated today exists, never its length or its shape. The librarian (milestone 5)
38
+ reads the series: it indexes it, ages it from git, and mines it, since an incident nothing in the
39
+ curated layer points back at is a candidate for the `codify` skill.
40
+ _(Provenance: Cognition — handoffs record decisions and their rationale; Git's own SubmittingPatches —
41
+ a commit's body explains the problem and justifies the change. See `../templates/handoff.md`.)_
49
42
  - **Subagents are context firewalls.** Fan work out to a persona with its own window and return only
50
43
  the conclusion; the parent's budget stays clean. Read in parallel, write from one place. _(See
51
44
  `../personas/`. Provenance: HumanLayer; Cognition — read-parallel / write-isolated.)_
@@ -46,6 +46,15 @@ which is worse than no check at all. So the breach is a rail and the *remedy* is
46
46
  gate holds. Written down rather than implied, because a rule nothing checks that is presented as one
47
47
  that does is the failure this page would otherwise be demonstrating.
48
48
 
49
+ **The cap can bind forward only** (Workspace Definition 2.11): with `memory.store.budget.cutoff`
50
+ declared beside it, it binds the records dated after the cutoff, and one dated on or before it is
51
+ reported, never railed, in proposal `0037`'s shape for a handoff. A record's date is its `**dated:**`
52
+ line, the day its text last changed, because a verify rail may not read git; an edit re-dates it, so
53
+ an old record meets the cap in the change that next rewrites it, while compression is that change's
54
+ to do. A record whose date cannot be read is refused. The cutoff never moves earlier, and moves later
55
+ only with a cap that tightens: like the raise above, a rule the human gate holds, because a moved
56
+ cutoff unbinds what the cap bound and only history shows the move.
57
+
49
58
  ## One fact per memory, with provenance
50
59
 
51
60
  A memory holds one fact and states where it came from. A rule links to the **incident** that created
@@ -62,8 +71,8 @@ curated context improves it, and the difference is measurable. So capture is age
62
71
  consolidation into the curated layer is a reviewed step. _(Provenance: ETH Zurich, arXiv:2602.11988 —
63
72
  generated context hurts, curated helps. The scheduled librarian arrived in milestone 5: a pass that
64
73
  regenerates each generated index, ages every record from git, nags a sealed stamp's owner, chases
65
- undecided proposals, drafts demotions, and **runs consolidation** — filed as a pull request nobody has
66
- to remember to open. What consolidation means on a schedule is bounded rather than claimed: the pass
74
+ undecided proposals, drafts demotions, and **runs consolidation** — its report filed with a pull request
75
+ nobody has to remember to open whenever the pass changed the tree. What consolidation means on a schedule is bounded rather than claimed: the pass
67
76
  reports how close each budget is to its rail, and surfaces records citing one incident as a
68
77
  **question**, because `../skills/consolidate/SKILL.md` merges records that are one *mechanism* and one
69
78
  incident routinely teaches several. Its steps 3 and 4 — surfacing contradictions and compressing what
@@ -145,7 +154,7 @@ where a reader already knows to look is stated at the narrowest carrier.
145
154
  was put to him twice and answered two different ways. The record of both, and of the third option that
146
155
  reconciles them, is
147
156
  [#98](https://github.com/sleepy-panda-srl/portulan/issues/98) and
148
- [`docs/milestones/m06.md`](../../docs/milestones/m06.md). Neither row needed a criterion edit to carry
157
+ [`docs/milestones/m06.md`](https://github.com/sleepy-panda-srl/portulan/blob/8a33f9b/docs/milestones/m06.md). Neither row needed a criterion edit to carry
149
158
  this: both already demanded their half. Said plainly because the
150
159
  alternative is a page describing an enforcement that does not exist — the failure a workspace's
151
160
  definition of done exists to catch (`.portulan/dod.md` condition 4, in this repository): a document may
@@ -0,0 +1,100 @@
1
+ # Sessions
2
+
3
+ > Core doctrine — on-read. Every fresh context pays for the host's own system prompt and tools, and for
4
+ > the always tier, at the cache's write price, unless an earlier session left that prefix in the cache.
5
+ > On Claude Code that prefix comes before any workspace file, and cache writes were most of what a task
6
+ > cost on this repository ([`../../evals/ab/warm.md`](../../evals/ab/warm.md) has the figures). So a task
7
+ > runs in as few fresh contexts as it can, each starts warm where it can, the cache lifetime fits the gap
8
+ > between uses, and a host switch that changes the prefix is earned by an A/B run.
9
+ > [`context.md`](context.md) governs what the host loads; this page governs how sessions start and end
10
+ > around it. _(Proposal [`0038`](../../.portulan/proposals/0038-what-a-change-spends-is-measured.md), rules
11
+ > 2 to 4, and row 12 of [`../../docs/plan.md`](../../docs/plan.md), whose compile targets carry the cache
12
+ > lifetime.)_
13
+
14
+ ## A session starts warm, or it pays the prefix again
15
+
16
+ **A fresh session starts warm when an earlier one left the same prefix in the cache**: the same host
17
+ version, tools, model and effort, the same instruction files and rules, the same working directory, and
18
+ nothing that varies per session ahead of the task, within the cache lifetime. A warm start reads the
19
+ prefix at the read price; a cold one writes it again. Sessions running side by side in one checkout
20
+ already share it. Sessions that follow one another share it only while nothing between them changed it,
21
+ and on a local checkout the host's startup git snapshot changes with every commit.
22
+
23
+ What moves the break earlier is anything per session in front of the task: a git snapshot, a date, a
24
+ working directory made fresh for each run, a hook's per-session output, a session's own id. Rule 3 of
25
+ `0038` keeps these out of Portulan's own always tier and hook text. A hosted session whose system prompt
26
+ names the session itself starts cold whatever a workspace compiles; that is inferred from such a prompt,
27
+ not documented.
28
+
29
+ ## Fewer fresh contexts per task
30
+
31
+ - **Continue until the restart threshold, then restart.** `0038`'s threshold is computed from the multipliers,
32
+ the fresh context and a horizon, which a workspace declares in `spend` (Workspace Definition 2.12) and which
33
+ are the general ones where it does not, and [`../../cli/advisory.mjs`](../../cli/advisory.mjs) says it once.
34
+ A restart that finds its prefix still cached costs less than the threshold assumes.
35
+ - **Resume a finished worker within its cache lifetime** rather than start a new one: its context is
36
+ read again rather than written again.
37
+ - **A subagent is a fresh context**, on a five-minute cache unless set. Fan out when the reads it keeps
38
+ out of the parent cost more than the prefix it writes ([`loop.md`](loop.md)'s firewall), not for one
39
+ read.
40
+ - A checkpoint, persona or ritual states what it loads ([`context.md`](context.md)), and the host's
41
+ prefix is part of that price.
42
+
43
+ ## The cache lifetime fits the gap
44
+
45
+ **Five minutes where the next use of the prefix comes within five minutes**: bursts, and headless runs that
46
+ follow one another. **An hour where sessions idle or the next one starts later.** A five-minute write costs
47
+ 1.25 times an uncached input token and an hour's 2, and every read renews either; a gap past the lifetime
48
+ writes the whole context again. On this repository's own tasks, run straight through, five-minute writes cut a
49
+ boot's cost by 22 to 30% and an edit's by about 18% against an hour's
50
+ ([`../../evals/ab/warm.md`](../../evals/ab/warm.md), row 2 and the measurement beside it); in a long session,
51
+ one pause past five minutes, while a person reads or a review or CI runs, can cost more than every write the
52
+ shorter lifetime saved. A subagent's lifetime is set apart from the session's.
53
+
54
+ ## A switch that changes the prefix is earned
55
+
56
+ Claude Code has three switches that change what a session writes: the git instructions (the startup
57
+ snapshot together with the commit and pull-request instructions), the cache lifetime, and, for a run
58
+ started headless, moving the per-machine sections (working directory, platform, memory paths, the
59
+ snapshot) out of the system prompt and into the first message, so runs in different directories or on
60
+ different machines share the system prompt. **Each stays off until an A/B run shows it cuts spend with no
61
+ loss**, which is `0038` rule 4's gate applied to sessions, and **a session that needs what a switch
62
+ removes keeps it**:
63
+
64
+ - **The git instructions are dropped only for sessions that neither commit nor open a pull request**, or
65
+ where the workspace's own guidance carries its commit and pull-request workflow. A session that needs
66
+ them is started with `CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS=0`, which outranks the compiled setting, and
67
+ a runner sets that per run.
68
+ - **The per-machine sections are moved, never dropped**: the exclusion puts them in the first message. It
69
+ applies to headless runs only, because only whatever starts a session can ask for it.
70
+ - **The cache lifetime removes nothing.** It trades the write price against the gap.
71
+
72
+ ## What a workspace declares
73
+
74
+ **`sessions` in the manifest**, since Workspace Definition 2.11
75
+ ([`../../spec/slots.md`](../../spec/slots.md)): `git_instructions` and `cache_lifetime` compile into the
76
+ host's project settings and reach every session in the repository; `headless` is what Portulan's own runners
77
+ apply to the sessions they start. Nothing is defaulted: an undeclared switch is the host's own default. This
78
+ repository declares one switch, five-minute writes for its headless runs, which row 2 of
79
+ [`../../evals/ab/warm.md`](../../evals/ab/warm.md) earned. Its interactive sessions keep the host's default,
80
+ because they commit and idle on reviews and CI; the git instructions wait on that page's row 3, and the
81
+ per-machine sections on its row 4.
82
+
83
+ ## What is machinery today, and what is not
84
+
85
+ `compile` emits the two interactive keys and says so on every run, and writes `spend`'s figures onto the
86
+ restart advisory's commands; `doctor` checks the shape and version of `sessions` and `spend`, and reports
87
+ every switch's state in one line; `init` offers an adopter's repository the five-minute lifetime with its
88
+ reason and its trade-off, and writes the key on a yes for `compile` to carry, and `upgrade` prints the same
89
+ offer and writes nothing. [`../../cli/warm.mjs`](../../cli/warm.mjs) runs fresh headless sessions in sequence
90
+ and prices each from the host's own usage records, warm against cold, and
91
+ [`../../evals/ab/warm.md`](../../evals/ab/warm.md) is its specification and its record. **Still to land**: the
92
+ interactive A/B, which needs real local sessions and so runs on a maintainer's device; a report of how often
93
+ sessions start warm, from the ledger's first request per context; an offer of the other switches, once their
94
+ rows have run; and carrying the key through `vendor`. Until each lands, the rule it would enforce is held by
95
+ the human gate.
96
+
97
+ _(Provenance: the maintainer's five-run measurement of 2026-09-24, whose figures the record in
98
+ [`../../evals/ab/warm.md`](../../evals/ab/warm.md) carries as its before, and the cache test of the same day,
99
+ which ran its rows 1 and 2; the techniques survey before it, from Claude Code's documentation on prompt
100
+ caching, settings and the CLI; and each switch read in Claude Code 2.1.281's program text on 2026-09-24.)_
@@ -49,8 +49,8 @@ Re-expressed from public practice (Letta — consolidation; compounding engineer
49
49
  — curated beats generated). Invocable on demand, and **schedulable as of milestone 5**: `cli/librarian.mjs`
50
50
  runs reindex over both generated indexes, staleness (the sealed-stamp re-validation nag, and record age
51
51
  from git), proposal nagging, demotion drafts, **mining** and **consolidation**, and renders what it
52
- found as a record a workspace's scheduler files as a pull request — this persona is the contract it
53
- executes.
52
+ found as a report, which a workspace's scheduler files with the pull request the pass opens when it
53
+ changed the tree — this persona is the contract it executes.
54
54
 
55
55
  **Every subject in the charter above is now reached on a cadence, and one of them is reached less
56
56
  deeply than the charter describes — which is the distinction between this persona and that tool.** A
@@ -18,8 +18,10 @@ description: Bring a memory store back under its budget by merging, compressing,
18
18
  decides which move applies**, and the finding names it: the index's `lines` (too many records —
19
19
  merge or retire; compressing removes no line and splitting adds one), the store's `kilobytes` (too
20
20
  much in total), or a record's `record_kilobytes` (one record too large — split, compress, or demote;
21
- step 2 and step 4). A fourth red, the index's `columns`, is **not** this pass's work: one over-long
22
- line is repaired by renaming the record, and nothing is consolidated.
21
+ step 2 and step 4), which binds only the records dated after a declared `cutoff`: one dated on or
22
+ before it is reported, never red, and meets the cap when its text next changes. A fourth red, the
23
+ index's `columns`, is **not** this pass's work: one over-long line is repaired by renaming the record,
24
+ and nothing is consolidated.
23
25
  - Two records say the same thing, or say opposite things.
24
26
  - A record's `Retire when:` condition has fired.
25
27
  - The librarian's scheduled pass runs.