@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.
- package/README.md +7 -6
- package/cli/README.md +142 -291
- package/cli/advisory.mjs +354 -0
- package/cli/comments.mjs +932 -0
- package/cli/compile.mjs +1443 -1210
- package/cli/context.mjs +883 -0
- package/cli/control-chars.mjs +12 -301
- package/cli/discover.mjs +17 -574
- package/cli/doctor.mjs +225 -1071
- package/cli/drills.mjs +1107 -0
- package/cli/feedback.mjs +16 -237
- package/cli/finish.mjs +437 -0
- package/cli/form.mjs +644 -0
- package/cli/fuzz-shell.mjs +595 -0
- package/cli/gate.mjs +7 -198
- package/cli/goldens.mjs +311 -0
- package/cli/index.mjs +305 -655
- package/cli/init.mjs +244 -564
- package/cli/inside.mjs +3 -28
- package/cli/instructions.mjs +829 -0
- package/cli/ledger.mjs +778 -0
- package/cli/librarian.mjs +135 -599
- package/cli/manifest.mjs +2 -39
- package/cli/mutants.mjs +764 -0
- package/cli/new.mjs +12 -243
- package/cli/pack-identity.mjs +6 -68
- package/cli/pack-version.mjs +13 -248
- package/cli/plugin-lint.mjs +42 -658
- package/cli/portulan.mjs +5 -124
- package/cli/recipe-set.mjs +16 -240
- package/cli/release-eval.mjs +748 -0
- package/cli/review-meter.mjs +500 -0
- package/cli/rule-carriers.mjs +12 -175
- package/cli/sessions.mjs +82 -0
- package/cli/skill-goldens.mjs +424 -0
- package/cli/skills-set.mjs +12 -414
- package/cli/stop-gate.mjs +126 -454
- package/cli/symbols.mjs +1132 -0
- package/cli/telemetry.mjs +582 -0
- package/cli/upgrade.mjs +171 -314
- package/cli/vendor.mjs +146 -476
- package/cli/version-carriers.mjs +9 -74
- package/core/README.md +1 -1
- package/core/engine.md +3 -9
- package/core/operating/autonomy.md +1 -1
- package/core/operating/context.md +183 -0
- package/core/operating/evolution.md +3 -3
- package/core/operating/loop.md +13 -20
- package/core/operating/memory.md +12 -3
- package/core/operating/sessions.md +100 -0
- package/core/personas/librarian.md +2 -2
- package/core/skills/consolidate/SKILL.md +4 -2
- package/core/templates/gate-policy.md +27 -8
- package/core/templates/handoff.md +28 -15
- package/core/templates/memory-entry.md +1 -0
- package/evals/README.md +320 -0
- package/evals/releases/0.1.3.json +134 -0
- package/evals/releases/0.1.3.md +64 -0
- package/evals/releases/0.2.0.json +146 -0
- package/evals/releases/0.2.0.md +67 -0
- package/evals/releases/README.md +32 -0
- package/package.json +9 -2
- package/packs/rituals/checkpoints/README.md +9 -2
- package/packs/rituals/checkpoints/pack.json +1 -1
- package/packs/rituals/checkpoints/skills/pre-commit/SKILL.md +32 -2
- package/packs/tools/github/verify/actions-pinned.sh +10 -55
- package/spec/README.md +112 -6
- package/spec/migrations/0001-repository-declares-its-tree.mjs +1 -26
- package/spec/migrations/0002-bundle-fallback-path.mjs +13 -74
- package/spec/migrations/0003-handoff-index-not-kept.mjs +66 -0
- package/spec/migrations/0004-changelog-fragments.mjs +53 -0
- package/spec/migrations/0005-session-log-retired.mjs +69 -0
- package/spec/migrations/0006-boot-card.mjs +77 -0
- package/spec/migrations/0007-guidance-compiled.mjs +31 -0
- package/spec/migrations/0008-card-reading.mjs +55 -0
- package/spec/migrations/0009-instruction-sections-on-read.mjs +60 -0
- package/spec/migrations/0010-comment-rail.mjs +57 -0
- package/spec/migrations/README.md +45 -10
- package/spec/pack.schema.json +1 -1
- package/spec/slots.md +359 -23
- package/spec/workspace.schema.json +145 -4
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
{
|
|
2
|
+
"portulan": {
|
|
3
|
+
"releaseEval": "1"
|
|
4
|
+
},
|
|
5
|
+
"version": "0.2.0",
|
|
6
|
+
"captured": "2026-09-30",
|
|
7
|
+
"source": {
|
|
8
|
+
"commit": "0b150002910d34a0a79408e82439cb81e1a0a0b9",
|
|
9
|
+
"clean": true
|
|
10
|
+
},
|
|
11
|
+
"host": {
|
|
12
|
+
"node": "v26.9.0",
|
|
13
|
+
"platform": "darwin"
|
|
14
|
+
},
|
|
15
|
+
"recipes": [
|
|
16
|
+
{
|
|
17
|
+
"id": "docs",
|
|
18
|
+
"exit": 0
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "json",
|
|
22
|
+
"exit": 0
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "doctor",
|
|
26
|
+
"exit": 0
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "tests",
|
|
30
|
+
"exit": 0
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "plugin",
|
|
34
|
+
"exit": 0
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "compile",
|
|
38
|
+
"exit": 0
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "workflow-filters",
|
|
42
|
+
"exit": 0
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "index",
|
|
46
|
+
"exit": 0
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "context",
|
|
50
|
+
"exit": 0
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "ledger",
|
|
54
|
+
"exit": 0
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"id": "control-chars",
|
|
58
|
+
"exit": 0
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "comments",
|
|
62
|
+
"exit": 0
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"id": "rule-carriers",
|
|
66
|
+
"exit": 0
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"id": "pack-version",
|
|
70
|
+
"exit": 0
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "pack-identity",
|
|
74
|
+
"exit": 0
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"id": "payload",
|
|
78
|
+
"exit": 0
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"id": "eval-bundle",
|
|
82
|
+
"exit": 0
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"id": "goldens",
|
|
86
|
+
"exit": 0
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "mutants",
|
|
90
|
+
"exit": 0
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"id": "fuzz-shell",
|
|
94
|
+
"exit": 0
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"id": "version-carriers",
|
|
98
|
+
"exit": 0
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"id": "skill-goldens",
|
|
102
|
+
"exit": 0
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"id": "review-loop",
|
|
106
|
+
"exit": 0
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"id": "telemetry",
|
|
110
|
+
"exit": 0
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"id": "drills",
|
|
114
|
+
"exit": 0
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"id": "ab",
|
|
118
|
+
"exit": 0
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"id": "ab-grade",
|
|
122
|
+
"exit": 0
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"id": "ab-run",
|
|
126
|
+
"exit": 0
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"id": "tools/github:actions-pinned",
|
|
130
|
+
"exit": 0
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
"excluded": [
|
|
134
|
+
{
|
|
135
|
+
"id": "release-eval",
|
|
136
|
+
"why": "a capture cannot be accurate about the record it is inside — captured before the record is written it is red for the record's absence, and captured after it is stale the moment the register is rendered. Its verdict for this release is the rail's own run on the pull request carrying this file."
|
|
137
|
+
}
|
|
138
|
+
],
|
|
139
|
+
"abBaseline": {
|
|
140
|
+
"snapshot": "evals/ab/baseline.json",
|
|
141
|
+
"register": "evals/ab/baseline.md",
|
|
142
|
+
"captured": "2026-08-31T09:05:32.699Z",
|
|
143
|
+
"commit": "a642d551eec6c146c67ef0acfe3f8ba535c5ce6e",
|
|
144
|
+
"clean": false
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Eval result — Portulan 0.2.0
|
|
2
|
+
|
|
3
|
+
> **Generated. Do not edit.** Rendered from [`evals/releases/0.2.0.json`](0.2.0.json) by [`cli/release-eval.mjs`](../../cli/release-eval.mjs) and byte-compared by the `release-eval` verify recipe. This is the eval result milestone 8 requires a release to carry — `docs/plan.md`, Protocol → Versioning.
|
|
4
|
+
|
|
5
|
+
## What this release was measured at
|
|
6
|
+
|
|
7
|
+
| | |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Version | `0.2.0` |
|
|
10
|
+
| Captured | 2026-09-30 |
|
|
11
|
+
| Commit | `0b150002910d34a0a79408e82439cb81e1a0a0b9` |
|
|
12
|
+
| Tree at capture | clean |
|
|
13
|
+
| Node | `v26.9.0` |
|
|
14
|
+
| Platform | `darwin` |
|
|
15
|
+
|
|
16
|
+
## The rails, and what each returned
|
|
17
|
+
|
|
18
|
+
29 of the 30 recipes this workspace yielded at capture. The set is derived from [`cli/recipe-set.mjs`](../../cli/recipe-set.mjs) and never listed by hand.
|
|
19
|
+
|
|
20
|
+
| Recipe | Verdict |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `docs` | green |
|
|
23
|
+
| `json` | green |
|
|
24
|
+
| `doctor` | green |
|
|
25
|
+
| `tests` | green |
|
|
26
|
+
| `plugin` | green |
|
|
27
|
+
| `compile` | green |
|
|
28
|
+
| `workflow-filters` | green |
|
|
29
|
+
| `index` | green |
|
|
30
|
+
| `context` | green |
|
|
31
|
+
| `ledger` | green |
|
|
32
|
+
| `control-chars` | green |
|
|
33
|
+
| `comments` | green |
|
|
34
|
+
| `rule-carriers` | green |
|
|
35
|
+
| `pack-version` | green |
|
|
36
|
+
| `pack-identity` | green |
|
|
37
|
+
| `payload` | green |
|
|
38
|
+
| `eval-bundle` | green |
|
|
39
|
+
| `goldens` | green |
|
|
40
|
+
| `mutants` | green |
|
|
41
|
+
| `fuzz-shell` | green |
|
|
42
|
+
| `version-carriers` | green |
|
|
43
|
+
| `skill-goldens` | green |
|
|
44
|
+
| `review-loop` | green |
|
|
45
|
+
| `telemetry` | green |
|
|
46
|
+
| `drills` | green |
|
|
47
|
+
| `ab` | green |
|
|
48
|
+
| `ab-grade` | green |
|
|
49
|
+
| `ab-run` | green |
|
|
50
|
+
| `tools/github:actions-pinned` | green |
|
|
51
|
+
|
|
52
|
+
**`release-eval` is excluded**, and the reason is printed rather than the row silently dropped: a capture cannot be accurate about the record it is inside — captured before the record is written it is red for the record's absence, and captured after it is stale the moment the register is rendered. Its verdict for this release is the rail's own run on the pull request carrying this file.
|
|
53
|
+
|
|
54
|
+
## The A/B baseline this release ships against
|
|
55
|
+
|
|
56
|
+
[`evals/ab/baseline.md`](https://github.com/sleepy-panda-srl/portulan/blob/v0.2.0/evals/ab/baseline.md), rendered from `evals/ab/baseline.json` — captured 2026-08-31T09:05:32.699Z at `a642d551eec6c146c67ef0acfe3f8ba535c5ce6e`, over a tree that was **not clean**.
|
|
57
|
+
|
|
58
|
+
**Its figures are not repeated here.** They have one carrier and this is not it.
|
|
59
|
+
|
|
60
|
+
## What this record does not establish
|
|
61
|
+
|
|
62
|
+
- **A green says this record agrees with its own capture — never that the release is good.** Every line below is what the recipes returned at one commit, and a recipe's own green establishes only what that recipe's documentation says it establishes.
|
|
63
|
+
- **The recipes were not run at the tag.** They ran at `0b150002910d34a0a79408e82439cb81e1a0a0b9`, and this record is committed *in* the cut change — so the tagged tree is this commit plus whatever that change still adds after the capture, the record itself at minimum. A record cannot be captured at a commit that does not exist yet, and printing the one it was captured at is the only honest form.
|
|
64
|
+
- **`release-eval` is excluded from the rows above**, because a capture cannot be accurate about the record it is inside. Its verdict for this release is the rail's own run on the pull request that carries this file.
|
|
65
|
+
- **This record measures the Portulan repository's own build**, at the commit named above — never the workspace, project or package this release is installed into. Nothing here has looked at your tree.
|
|
66
|
+
- **The tree was clean at capture**, so the commit named above is what was measured.
|
|
67
|
+
- **The A/B baseline's figures are NOT restated here.** This record names which baseline the release ships against; the figures and everything that may not be concluded from them live in that register alone.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# `evals/releases/`
|
|
2
|
+
|
|
3
|
+
The eval result each release carries, one pair per version — milestone 8's ninth clause. The limits are
|
|
4
|
+
in [`../README.md`](../README.md); the argument, the mechanism and the two designs a fresh context
|
|
5
|
+
reversed are in [`../../cli/release-eval.mjs`](../../cli/release-eval.mjs). This file does not restate
|
|
6
|
+
either, because a rule with two carriers is obeyed at the narrower one.
|
|
7
|
+
|
|
8
|
+
- `<version>.json` — the **capture**: the verdict every recipe the workspace yielded returned, at a named
|
|
9
|
+
commit, plus the identity of the A/B baseline that release ships against. Written by
|
|
10
|
+
`node cli/release-eval.mjs --capture`, which runs them. **A hand-edited capture is NOT caught by the
|
|
11
|
+
byte comparison** — that holds the register to the capture, so `--write` re-renders the edit and the
|
|
12
|
+
pair agrees perfectly. What guards a capture is the rail's reading of its *contents*: an exclusion
|
|
13
|
+
list that is anything but the self-exclusion, a recorded non-zero exit, a version not matching the
|
|
14
|
+
release it is filed under, a record for a version never cut or for one the clause does not govern.
|
|
15
|
+
This sentence used to claim the opposite, and a sentence claiming a guard that does not exist is what
|
|
16
|
+
makes the gap it describes look unreachable.
|
|
17
|
+
- `<version>.md` — the **register**, rendered from the capture. Generated; do not edit. `--write`
|
|
18
|
+
re-renders it and the `release-eval` recipe refuses any difference.
|
|
19
|
+
|
|
20
|
+
**Which releases have records is not written here.** `ls` this directory, or run
|
|
21
|
+
`node cli/release-eval.mjs --verify`, which prints the governed set and is the carrier — a sentence
|
|
22
|
+
counting the files beside it would be a hand-maintained figure of a subject that grows once per release,
|
|
23
|
+
which is the defect this repository deletes on sight.
|
|
24
|
+
|
|
25
|
+
**What is fixed, and is the only thing this file states about coverage:** the clause binds *from
|
|
26
|
+
milestone 8* — [`../../docs/plan.md`](../../docs/plan.md), Protocol → Versioning — so `v0.1.0`, `v0.1.1`
|
|
27
|
+
and `v0.1.2` carry none and are never asked to. That is history and cannot change.
|
|
28
|
+
|
|
29
|
+
_(This paragraph read **"This directory is empty of records"** as a standing claim. It would have gone
|
|
30
|
+
false in the very commit that wrote the first record — and, since this directory ships inside the npm
|
|
31
|
+
payload, it would then have been **frozen false beside the record it denied**, in a tarball only another
|
|
32
|
+
release could correct. Caught by a second opinion before the first cut rather than by a reader of it.)_
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sleepy_panda_srl/portulan",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Portulan — an agentic-engineering framework whose product is its files. The CLI wraps the file-based mechanics for hosts beyond Claude Code.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -15,13 +15,20 @@
|
|
|
15
15
|
"core/",
|
|
16
16
|
"spec/",
|
|
17
17
|
"packs/",
|
|
18
|
+
"evals/releases/",
|
|
18
19
|
"README.md",
|
|
19
20
|
"LICENSE",
|
|
20
21
|
"NOTICE",
|
|
21
22
|
"!cli/*.test.mjs",
|
|
22
23
|
"!cli/fixtures",
|
|
23
24
|
"!cli/eval-bundle.mjs",
|
|
24
|
-
"!cli/eval-license.template.md"
|
|
25
|
+
"!cli/eval-license.template.md",
|
|
26
|
+
"!cli/ab.mjs",
|
|
27
|
+
"!cli/ab-run.mjs",
|
|
28
|
+
"!cli/ab-grade.mjs",
|
|
29
|
+
"!cli/warm.mjs",
|
|
30
|
+
"!cli/payload.mjs",
|
|
31
|
+
"!cli/roster.mjs"
|
|
25
32
|
],
|
|
26
33
|
"repository": {
|
|
27
34
|
"type": "git",
|
|
@@ -38,7 +38,7 @@ Four verdicts, and which are available depends on the checkpoint.
|
|
|
38
38
|
| Verdict | Means | Available at |
|
|
39
39
|
|---|---|---|
|
|
40
40
|
| **APPROVE** | Proceed as it stands. | session-open · pre-commit |
|
|
41
|
-
| **APPROVE-WITH-ADJUSTMENTS** | Proceed once the numbered adjustments are folded in. Not a soft failure — it is what a working checkpoint usually returns. | session-open · pre-commit |
|
|
41
|
+
| **APPROVE-WITH-ADJUSTMENTS** | Proceed once the numbered adjustments are folded in. Not a soft failure — it is what a working checkpoint usually returns. **At pre-commit the fold itself is then graded** — see that skill. | session-open · pre-commit |
|
|
42
42
|
| **REQUEST-CHANGES** | Does not meet the standard. Name what, and what would meet it. Needs a second pass at the same checkpoint. | all three |
|
|
43
43
|
| **CLOSE** | Every clause of the criterion is demonstrated; the milestone may be marked done. | milestone-close only |
|
|
44
44
|
|
|
@@ -49,6 +49,13 @@ concerns is not a verdict.
|
|
|
49
49
|
proceed on a promise to fold something in, and a close is the one decision with nothing downstream of
|
|
50
50
|
it to catch what was folded in wrongly.
|
|
51
51
|
|
|
52
|
+
**That argument reaches one moment earlier, which is why pre-commit's A-W-A grades its own fold**
|
|
53
|
+
(proposal `0035`, 2026-08-29). Session-open's fold *does* have something downstream — the plan it folds
|
|
54
|
+
into becomes a diff, and pre-commit grades that. **Pre-commit's fold has only the commit.** So the
|
|
55
|
+
verdict that lets work proceed on a promise is, at that moment, a promise nothing checks; measured on a
|
|
56
|
+
change where two of three later review findings were created by the fold and were absent from the graded
|
|
57
|
+
diff.
|
|
58
|
+
|
|
52
59
|
## The fidelity note
|
|
53
60
|
|
|
54
61
|
What a close leaves in the record once the session that wrote it is gone. Four parts, in one clause:
|
|
@@ -93,7 +100,7 @@ under, and the checkpoint section of that project's own gate map.
|
|
|
93
100
|
|
|
94
101
|
**The evidence is that project's build record**, cited as provenance rather than as procedure: the
|
|
95
102
|
fresh-context checkpoints changed *designs* rather than wording at two consecutive milestones — the
|
|
96
|
-
story is in [milestone 4's record](https://github.com/sleepy-panda-srl/portulan/blob/
|
|
103
|
+
story is in [milestone 4's record](https://github.com/sleepy-panda-srl/portulan/blob/8a33f9b/docs/milestones/m04.md),
|
|
97
104
|
beside [the protocol itself](https://github.com/sleepy-panda-srl/portulan/blob/main/docs/plan.md) —
|
|
98
105
|
while adopters received no artifact for the ritual that produced them. This pack is that artifact.
|
|
99
106
|
|
|
@@ -65,8 +65,38 @@ whether a status column changed.
|
|
|
65
65
|
## The verdicts this checkpoint may return
|
|
66
66
|
|
|
67
67
|
- **APPROVE** — commit as it stands.
|
|
68
|
-
- **APPROVE-WITH-ADJUSTMENTS** —
|
|
69
|
-
|
|
68
|
+
- **APPROVE-WITH-ADJUSTMENTS** — fold the numbered adjustments, then **grade the fold**: a pass over
|
|
69
|
+
**the fold delta alone**, in a context that did not perform the fold, before the commit.
|
|
70
|
+
- **REQUEST-CHANGES** — the diff does not meet the criterion; it needs work and a second pre-commit pass
|
|
71
|
+
**over the whole diff**.
|
|
72
|
+
|
|
73
|
+
**Grade a fully staged diff.** The whole procedure below is index-shaped, so anything left unstaged was
|
|
74
|
+
not graded and will not appear in the fold delta either — it would pass through both nets. Stage first,
|
|
75
|
+
then grade; this is what practice here already did, and it is stated because the mechanism now depends
|
|
76
|
+
on it rather than merely benefiting from it.
|
|
77
|
+
|
|
78
|
+
**A checkpoint records the tree it graded** — the index it read, as a tree object (`git write-tree`). The
|
|
79
|
+
fold delta is then **`git diff --cached <graded-tree>`**: the recorded tree against the index, which is
|
|
80
|
+
what is about to be committed. Derived, not reconstructed from memory by the person who folded.
|
|
81
|
+
|
|
82
|
+
_Why the fold is graded at all, and why the A-W-A pass is scoped to the delta._ **The tree that ships is
|
|
83
|
+
not the tree that was graded**, and the difference is where a remediation can inject what the checkpoint
|
|
84
|
+
just caught elsewhere. Measured: on one change, folding an adjustment split a table row and left a
|
|
85
|
+
summary count stale three sections above, and folding another **re-measured a question the checkpoint had
|
|
86
|
+
answered correctly** — with a line-based tool against a normalising scanner — and wrote the wrong answer
|
|
87
|
+
over the right one. Neither defect was in the graded diff. A full re-grade after every A-W-A would double
|
|
88
|
+
the cost of the cheapest verdict, which is the ceremony a definition of done should refuse; **the delta is
|
|
89
|
+
the only region a fold-injected defect can be in.**
|
|
90
|
+
|
|
91
|
+
_The delta is **whatever was staged after the grade** — it *should* be the adjustments and nothing else,
|
|
92
|
+
and the mechanism does not guarantee that. **Where it is wider, that is not noise; it is the finding.**
|
|
93
|
+
Post-grade edits nobody proposed are exactly what escapes a checkpoint today, so a pass that sees them is
|
|
94
|
+
worth more than one over a delta that had been trimmed to what the verdict asked for._
|
|
95
|
+
|
|
96
|
+
_What this does not buy, said plainly: whether a fresh context actually read the delta is not checkable —
|
|
97
|
+
a session can claim a pass it did not run. What changes is that the pass now has a **defined subject**,
|
|
98
|
+
derived from the tree rather than recalled by the one who folded._ Provenance: proposal `0035`,
|
|
99
|
+
accepted 2026-08-29.
|
|
70
100
|
|
|
71
101
|
## Why it earns its tokens
|
|
72
102
|
|
|
@@ -1,34 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
2
|
# Every `uses:` in a GitHub Actions workflow is pinned to a full 40-character commit SHA.
|
|
4
|
-
#
|
|
5
|
-
# Exit 0 green · 1 red · 2 could not run. The third is load-bearing here, as everywhere in this
|
|
6
|
-
# project: a recipe that could not look must never report what a recipe that looked and found nothing
|
|
7
|
-
# reports. See `README.md` beside this file for what the check does NOT cover and why it exists at all
|
|
8
|
-
# given that GitHub can enforce pinning itself.
|
|
9
|
-
#
|
|
10
|
-
# Composed into an adopting workspace through `pack.json`'s `contributes.verify`, whose `run` names
|
|
11
|
-
# this file through `${PACK_ROOT}` — the pack does not know the adopter's layout and must not guess it.
|
|
3
|
+
# Exit 0 green · 1 red · 2 could not run.
|
|
12
4
|
|
|
13
5
|
set -uo pipefail
|
|
14
6
|
|
|
7
|
+
# Relative to the working directory, the adopter's root: where the pack sits says nothing about it.
|
|
15
8
|
WORKFLOWS=".github/workflows"
|
|
16
9
|
|
|
17
|
-
#
|
|
18
|
-
# The tools this recipe invokes, checked before it invokes them.
|
|
19
|
-
#
|
|
20
|
-
# Without this the failure is SILENT and GREEN: the two loops below read from process substitutions,
|
|
21
|
-
# so a missing `find` or `grep` yields no lines at all, every counter stays zero, `status` stays 0 —
|
|
22
|
-
# and the recipe reports a pass having examined nothing. That is the false green — "nothing looked"
|
|
23
|
-
# reported as "nothing wrong" — and it is precisely what `.portulan/memory/verify-preconditions-fail-closed.md`
|
|
24
|
-
# exists to refuse. The header below already cited that rule for the workflows DIRECTORY and did not
|
|
25
|
-
# apply it to the recipe's own dependencies: a rule obeyed at one of its two sites, in a file that
|
|
26
|
-
# quotes the rule. Copilot round 5.
|
|
27
|
-
#
|
|
28
|
-
# `requires` in `pack.json` declares the same list. Two carriers, deliberately — the manifest's is
|
|
29
|
-
# what a host reads to decide whether to run at all, this one is what fires when it ran anyway — and
|
|
30
|
-
# they are checked against each other by nothing, which is stated here rather than implied.
|
|
31
|
-
# ---------------------------------------------------------------------------------------------
|
|
10
|
+
# `pack.json`'s `requires` lists these tools too, and nothing checks that the two agree.
|
|
32
11
|
missing=""
|
|
33
12
|
for tool in find grep sed sort cut wc tr; do
|
|
34
13
|
command -v "$tool" >/dev/null 2>&1 || missing="$missing $tool"
|
|
@@ -40,16 +19,6 @@ if [ -n "$missing" ]; then
|
|
|
40
19
|
exit 2
|
|
41
20
|
fi
|
|
42
21
|
|
|
43
|
-
# ---------------------------------------------------------------------------------------------
|
|
44
|
-
# Preconditions fail closed. Enumeration is the precondition here exactly as it is in this project's
|
|
45
|
-
# own recipes (`.portulan/memory/verify-preconditions-fail-closed.md`): a green computed over a
|
|
46
|
-
# directory that was not there is a green about nothing.
|
|
47
|
-
#
|
|
48
|
-
# Both the absent directory and the present-but-empty one are could-not-run rather than vacuously
|
|
49
|
-
# green, and the distinction between them is kept in the MESSAGE rather than in the exit code —
|
|
50
|
-
# a workspace with no workflows has composed a pack that cannot do its job, and should hear that
|
|
51
|
-
# sentence rather than a green it might mistake for coverage.
|
|
52
|
-
# ---------------------------------------------------------------------------------------------
|
|
53
22
|
if [ ! -d "$WORKFLOWS" ]; then
|
|
54
23
|
printf 'actions-pinned: COULD NOT RUN — there is no %s directory in this repository.\n' "$WORKFLOWS" >&2
|
|
55
24
|
printf ' A workspace with no GitHub Actions workflows has nothing for this recipe to check;\n' >&2
|
|
@@ -57,8 +26,6 @@ if [ ! -d "$WORKFLOWS" ]; then
|
|
|
57
26
|
exit 2
|
|
58
27
|
fi
|
|
59
28
|
|
|
60
|
-
# `find` rather than a glob, so a directory holding only subdirectories is not mistaken for one
|
|
61
|
-
# holding workflows. Sorted so the report is stable between runs and between machines.
|
|
62
29
|
files=$(find "$WORKFLOWS" -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) | sort)
|
|
63
30
|
|
|
64
31
|
if [ -z "$files" ]; then
|
|
@@ -69,30 +36,21 @@ fi
|
|
|
69
36
|
status=0
|
|
70
37
|
examined=0
|
|
71
38
|
pinned=0
|
|
72
|
-
|
|
39
|
+
exempt=0
|
|
73
40
|
|
|
74
41
|
while IFS= read -r file; do
|
|
75
|
-
# One `uses:` per line
|
|
76
|
-
# spelling would slip past this, which is stated in README.md as a known limit rather than
|
|
77
|
-
# covered by a matcher that would claim more than it reads.
|
|
42
|
+
# One `uses:` per line: a folded or flow-mapping spelling is not seen.
|
|
78
43
|
while IFS= read -r line; do
|
|
79
44
|
value=$(printf '%s\n' "$line" | sed -E 's/^[[:space:]]*-?[[:space:]]*uses:[[:space:]]*//; s/[[:space:]]*(#.*)?$//; s/^["'"'"']//; s/["'"'"']$//')
|
|
80
45
|
[ -n "$value" ] || continue
|
|
81
46
|
|
|
82
|
-
# `examined` counts only references this recipe can actually judge, and the exempt ones are
|
|
83
|
-
# counted separately below. Counting them here made the summary print a ratio like `2 of 3
|
|
84
|
-
# pinned` on a run where every non-exempt reference WAS pinned — a green whose own number
|
|
85
|
-
# looked like a partial pass. Copilot round 2.
|
|
86
47
|
case "$value" in
|
|
87
48
|
./*)
|
|
88
|
-
|
|
89
|
-
# commit to pin, and demanding one would be a check that cannot be satisfied.
|
|
90
|
-
local_refs=$((local_refs + 1))
|
|
49
|
+
exempt=$((exempt + 1))
|
|
91
50
|
continue
|
|
92
51
|
;;
|
|
93
52
|
docker://*)
|
|
94
|
-
|
|
95
|
-
local_refs=$((local_refs + 1))
|
|
53
|
+
exempt=$((exempt + 1))
|
|
96
54
|
continue
|
|
97
55
|
;;
|
|
98
56
|
esac
|
|
@@ -102,10 +60,7 @@ while IFS= read -r file; do
|
|
|
102
60
|
if [ "$ref" = "$value" ]; then
|
|
103
61
|
printf ' UNPINNED %s: `uses: %s` carries no `@ref` at all\n' "$file" "$value" >&2
|
|
104
62
|
status=1
|
|
105
|
-
# Case-insensitive
|
|
106
|
-
# mixed-case SHA is a pinned SHA. Matching lowercase only would have reported a correctly
|
|
107
|
-
# pinned action as unpinned — a FALSE RED, which is the failure mode this project treats as
|
|
108
|
-
# worse than a missed catch, because it is the one that gets a check switched off.
|
|
63
|
+
# Case-insensitive, as hex is: an uppercase SHA is still a pinned one.
|
|
109
64
|
elif printf '%s' "$ref" | grep -Eqi '^[0-9a-f]{40}$'; then
|
|
110
65
|
pinned=$((pinned + 1))
|
|
111
66
|
else
|
|
@@ -120,8 +75,8 @@ count=$(printf '%s\n' "$files" | wc -l | tr -d ' ')
|
|
|
120
75
|
if [ "$status" -eq 0 ]; then
|
|
121
76
|
printf 'actions-pinned: %s of %s `uses:` reference(s) pinned to a full commit SHA across %s workflow file(s)' \
|
|
122
77
|
"$pinned" "$examined" "$count"
|
|
123
|
-
if [ "$
|
|
124
|
-
printf ', %s local or container reference(s) exempt' "$
|
|
78
|
+
if [ "$exempt" -gt 0 ]; then
|
|
79
|
+
printf ', %s local or container reference(s) exempt' "$exempt"
|
|
125
80
|
fi
|
|
126
81
|
printf '\n\nGREEN — verify recipe passed.\n'
|
|
127
82
|
else
|
package/spec/README.md
CHANGED
|
@@ -114,7 +114,9 @@ carries, applied to the handoff series — `slots.handoffs` when `handoffs` is d
|
|
|
114
114
|
either counted as a handoff by the `record` check's correspondence or failed by it for carrying no
|
|
115
115
|
date; and, as of 2.6, that same pair a third time over the per-persona scope layer — `slots.personas`
|
|
116
116
|
when `personas` is declared, and `personas.index.path` resolving **outside** `slots.personas`, since
|
|
117
|
-
`index`'s orphan sweep would examine a file sited there as an undeclared persona location
|
|
117
|
+
`index`'s orphan sweep would examine a file sited there as an undeclared persona location; and, as of
|
|
118
|
+
2.11, `memory.store.budget.record_kilobytes` when `memory.store.budget.cutoff` is declared, since a
|
|
119
|
+
cutoff says which records that cap binds and alone configures nothing.
|
|
118
120
|
Everything else is optional.
|
|
119
121
|
|
|
120
122
|
_The 2.6 pair was enforced in `doctor` from the day the keys landed and was missing from this list until
|
|
@@ -139,7 +141,7 @@ a manifest in *another repository*, and no contract over a single document can s
|
|
|
139
141
|
join the list, and the reason is what keeps that list meaningful: every entry there is a dependency
|
|
140
142
|
between two keys **of one manifest**, which is the thing a reader of the schema alone would expect to find
|
|
141
143
|
and does not. A check that needs a second repository is not in that category, and folding it in would make
|
|
142
|
-
the count measure two different things at once. **The count stays at
|
|
144
|
+
the count measure two different things at once. **The count stays at ten**, and it counts what it always
|
|
143
145
|
counted.
|
|
144
146
|
|
|
145
147
|
[`slots.md`](slots.md) argues each one.
|
|
@@ -150,8 +152,8 @@ defect [#77](https://github.com/sleepy-panda-srl/portulan/issues/77) is filed ab
|
|
|
150
152
|
and adding to the list at 2.5 is exactly the edit that would have left it stale again — which is how
|
|
151
153
|
this one was noticed._
|
|
152
154
|
|
|
153
|
-
That the count went from one to three in a single MINOR, then to five, then to seven,
|
|
154
|
-
**
|
|
155
|
+
That the count went from one to three in a single MINOR, then to five, then to seven, then to nine, and
|
|
156
|
+
now stands at **ten**, is worth noticing rather than absorbing. Each is a genuine dependency between two keys and none
|
|
155
157
|
can be written in the declared subset, so the gap between *what the schema says* and *what a conforming
|
|
156
158
|
manifest must satisfy* is widening — and a constraint invisible to someone reading the schema alone is a
|
|
157
159
|
real cost, stated here rather than discovered. The subset earns its narrowness by being implementable
|
|
@@ -179,6 +181,38 @@ subset can say, and `doctor` returns at *the manifest must conform first* before
|
|
|
179
181
|
runs. It is a live hole for `cli/index.mjs` alone, which validates against no schema by design and
|
|
180
182
|
therefore refuses the string itself.)_
|
|
181
183
|
|
|
184
|
+
**2.9 adds nothing to that list either**: `context` requires `ratio`, and `ratio` requires its two
|
|
185
|
+
fields, in the schema itself, because `required` is in the subset. It adds **two** keys to the hand-check,
|
|
186
|
+
so it covers **nine**. `context.always.budget.tokens` is the fifth budget the subset can only type as
|
|
187
|
+
`number`. `context.ratio.bytes_per_token` is the first key held to something other than a positive
|
|
188
|
+
integer: 2.99 bytes per token is a real figure, so the check is *at least 1*, since a token covers at
|
|
189
|
+
least one byte and a smaller figure is tokens per byte entered inverted. For that key `minimum` alone
|
|
190
|
+
would close the gap.
|
|
191
|
+
|
|
192
|
+
**2.10 adds nothing to either list.** `slots.context` is a directory path, which the schema types in full,
|
|
193
|
+
and what a unit's frontmatter may say is `compile`'s to refuse, because the subset cannot see inside a
|
|
194
|
+
file. `doctor` checks the slot as it checks every directory slot, and gates it to 2.10.
|
|
195
|
+
|
|
196
|
+
**2.11 adds one to each list.** `memory.store.budget.cutoff` needs `record_kilobytes` beside it, the
|
|
197
|
+
tenth conditional requirement; and its `pattern` admits a day that does not exist, such as `2026-02-30`,
|
|
198
|
+
so `doctor` and `cli/index.mjs` refuse one by hand, and the hand-check covers **ten**.
|
|
199
|
+
`sessions` adds nothing to either: every field of it is a boolean or one of two strings, which the
|
|
200
|
+
schema types in full with `type` and `enum`, so `doctor` only gates the key to 2.11.
|
|
201
|
+
|
|
202
|
+
**2.12 adds five to the hand-check and nothing to the conditional requirements.** `required` holds both
|
|
203
|
+
halves of `spend.multipliers` and both lifetimes of its `write` in the schema itself, and the subset types
|
|
204
|
+
the four figures only as `number`, the horizon included, since it cannot say `integer`, so `doctor` holds
|
|
205
|
+
the read in (0, 1], each write at least 1 and the horizon to a positive integer by hand, and refuses a write
|
|
206
|
+
the read divides past the largest number, since the restart threshold divides one by the other; the
|
|
207
|
+
hand-check covers **fifteen**. For the two writes `minimum` alone would close the gap, as it would for the
|
|
208
|
+
ratio; the read is the first figure bounded above, so it would need `exclusiveMinimum` and `maximum`, and
|
|
209
|
+
the horizon the budgets' `minimum` and `integer`; no keyword in the subset relates two figures, so the
|
|
210
|
+
quotient stays by hand.
|
|
211
|
+
|
|
212
|
+
**2.13 adds nothing to either list.** `spend.restart` is one of two strings, which the subset types in full
|
|
213
|
+
with `type` and `enum`, so the key needs no hand-check, only the gate to 2.13, which `doctor` and `compile`
|
|
214
|
+
both hold.
|
|
215
|
+
|
|
182
216
|
_These figures are history rather than state: what 2.3 and 2.4 added cannot change, so they do not go
|
|
183
217
|
stale the way the removed count did. The one forward-looking sentence is the growth rate, and it is
|
|
184
218
|
dated by the version it names._
|
|
@@ -194,15 +228,87 @@ number governing both would make a bump in either mean a change in the other:
|
|
|
194
228
|
|
|
195
229
|
| Schema | Manifest key | Current | What it governs |
|
|
196
230
|
|---|---|---|---|
|
|
197
|
-
| [`workspace.schema.json`](workspace.schema.json) | `portulan.spec` | **2.
|
|
231
|
+
| [`workspace.schema.json`](workspace.schema.json) | `portulan.spec` | **2.13** | the Workspace Definition — the manifest at a workspace root |
|
|
198
232
|
| [`pack.schema.json`](pack.schema.json) | `portulan.pack` | **1.0** | the Pack Definition — the manifest at a pack root. |
|
|
199
233
|
|
|
200
234
|
The rules below apply to each train independently. `portulan.spec` is `MAJOR.MINOR`, and the current
|
|
201
|
-
Workspace Definition version is **2.
|
|
235
|
+
Workspace Definition version is **2.13**. It did **not** move when the Pack Definition arrived, because
|
|
202
236
|
`workspace.schema.json` was byte-identical across that change: `packs` already existed as an array of
|
|
203
237
|
strings and was deliberately left that way, since tightening its items to the canonical `category/name`
|
|
204
238
|
form would be a constraint an existing manifest could newly fail, which is a MAJOR.
|
|
205
239
|
|
|
240
|
+
**2.13 is a MINOR on 2.8's terms: optional keys only.** `spend.restart` is the last piece of item 5 of
|
|
241
|
+
proposal `0038`'s order of work, the declared block its ruling 3 allows: `"advise"` is the restart advisory as
|
|
242
|
+
it was, and `"block"` also holds a turn's end once, at the first stop at or past the threshold that no block
|
|
243
|
+
provoked, with the advisory's line as the reason; [`slots.md`](slots.md) argues it. Nothing is removed,
|
|
244
|
+
renamed, tightened or defaulted: without the key, or with `"advise"`, the settings compile byte for byte as
|
|
245
|
+
before, so every 2.12 manifest is a valid 2.13 manifest unchanged. This repository's own workspace stays on
|
|
246
|
+
2.11 and declares no block: it declares no multipliers either, and at the general ones the line comes early
|
|
247
|
+
for a model whose cache reads cost less than a tenth, so a block would hold its sessions short of their real
|
|
248
|
+
line. Nothing writes 2.13 unasked: `init` asks nothing about the key and `upgrade` offers nothing, since
|
|
249
|
+
ruling 3 makes a block a workspace's own choice. `KNOWN_SPECS` in [`../cli/index.mjs`](../cli/index.mjs) and
|
|
250
|
+
[`../cli/librarian.mjs`](../cli/librarian.mjs) gains `"2.13"` by addition, and `doctor` and `compile` both
|
|
251
|
+
refuse the key in a manifest that declares an earlier MINOR.
|
|
252
|
+
|
|
253
|
+
**2.12 is a MINOR on 2.8's terms: optional keys only.** `spend` is item 4 of proposal `0038`'s order of
|
|
254
|
+
work: the multipliers the restart threshold is computed at, which its ruling 2 has the manifest declare, and
|
|
255
|
+
the horizon the threshold is judged over, 20 requests under ruling 3 until the manifest declares another;
|
|
256
|
+
[`slots.md`](slots.md) argues it. Nothing is removed, renamed, tightened or defaulted: without `spend`,
|
|
257
|
+
[`../cli/ledger.mjs`](../cli/ledger.mjs) and the restart advisory price at the general multipliers as before
|
|
258
|
+
and say `undeclared`, and the settings compile byte for byte as before, so every 2.11 manifest is a valid
|
|
259
|
+
2.12 manifest unchanged. This repository's own workspace stays on 2.11 and declares no `spend`; it now
|
|
260
|
+
declares `sessions.headless.cache_lifetime` `"5m"`, a 2.11 key. `examples/` stays on 2.4. `init` writes 2.11
|
|
261
|
+
where a person takes the five-minute cache lifetime it now offers, since `sessions` is 2.11's key, and 2.10
|
|
262
|
+
otherwise. `KNOWN_SPECS` in [`../cli/index.mjs`](../cli/index.mjs) and
|
|
263
|
+
[`../cli/librarian.mjs`](../cli/librarian.mjs) gains `"2.12"` by addition, and `doctor` gates the key to
|
|
264
|
+
2.12. `0038`'s third key, the declared Stop-gate block, takes the next free MINOR, 2.13, in its own change,
|
|
265
|
+
and `0034`'s keys take the next free MINOR when they are drafted.
|
|
266
|
+
|
|
267
|
+
**2.11 is a MINOR on 2.8's terms: optional keys only.** `memory.store.budget.cutoff`
|
|
268
|
+
makes the per-record cap forward-only, in proposal `0037`'s shape for a handoff, and [`slots.md`](slots.md)
|
|
269
|
+
argues it. `sessions` is the machine half of [`../core/operating/sessions.md`](../core/operating/sessions.md),
|
|
270
|
+
from `0038`'s rules 3 and 4: the host switches that change what a fresh session writes into the prompt cache,
|
|
271
|
+
two of which `compile` writes into the settings it already generates and one of which only a runner can apply;
|
|
272
|
+
[`slots.md`](slots.md) argues it too. Nothing is removed, renamed, tightened or defaulted: without the cutoff,
|
|
273
|
+
`record_kilobytes` binds every record as it did, and without `sessions` the settings compile byte for byte as
|
|
274
|
+
before, so every 2.10 manifest is a valid 2.11 manifest unchanged. This repository's own workspace moves from
|
|
275
|
+
2.10 to 2.11 to declare the cutoff, with the cap lowered from 8 KB to 2 and the cutoff 2026-09-24, and declares
|
|
276
|
+
no `sessions`; `examples/` stays on 2.4. `KNOWN_SPECS` in [`../cli/index.mjs`](../cli/index.mjs) and
|
|
277
|
+
[`../cli/librarian.mjs`](../cli/librarian.mjs) gains `"2.11"` by addition, `doctor` gates both keys to 2.11,
|
|
278
|
+
and the four writers stay at `2.7`, since nothing they scaffold declares either. `0034`'s key and the rest of
|
|
279
|
+
`0038`'s still take the next free MINOR when they are drafted.
|
|
280
|
+
|
|
281
|
+
**2.10 is a MINOR on 2.9's terms: one optional slot and nothing else.** `slots.context` is the content
|
|
282
|
+
half of [`../core/operating/context.md`](../core/operating/context.md), from proposal `0036`'s compile
|
|
283
|
+
targets: a directory of guidance, each file declaring in its frontmatter the load tier it belongs in, which
|
|
284
|
+
`compile` emits in each host's own form. Nothing is removed, renamed, tightened or defaulted, so every 2.9
|
|
285
|
+
manifest is a valid 2.10 manifest unchanged, and [`slots.md`](slots.md) argues the slot. It is the first
|
|
286
|
+
MINOR with two digits, and every reader compares the MINOR as a number or matches the string whole, so
|
|
287
|
+
`2.10` sorts after `2.9`. `0034`'s and `0038`'s keys still take the next free MINOR when they are drafted.
|
|
288
|
+
|
|
289
|
+
**Measured across the bump:** `node cli/doctor.mjs .portulan examples` is GREEN, with `.portulan` on 2.8
|
|
290
|
+
and `examples/` on 2.4, both untouched: the demo stays six MINORs behind for the reason given under 2.8,
|
|
291
|
+
and no workspace here declares the slot, because moving guidance between tiers in this repository's own
|
|
292
|
+
curated layer is its maintainer's change to make. `KNOWN_SPECS` in [`../cli/index.mjs`](../cli/index.mjs)
|
|
293
|
+
and [`../cli/librarian.mjs`](../cli/librarian.mjs) gains `"2.10"` by addition, and the four writers stay at
|
|
294
|
+
`2.7`, since nothing they scaffold declares the slot.
|
|
295
|
+
|
|
296
|
+
**2.9 is a MINOR on 2.8's terms: one optional key and nothing else.** `context` is the machine half of
|
|
297
|
+
[`../core/operating/context.md`](../core/operating/context.md), from proposal `0036`: an always-tier
|
|
298
|
+
budget in tokens and the bytes-per-token ratio it is counted at, with the host whose exact count
|
|
299
|
+
calibrated it. Nothing is removed, renamed, tightened or defaulted, so every 2.8 manifest is a valid 2.9
|
|
300
|
+
manifest unchanged. [`slots.md`](slots.md) argues the key. **It carries `context` alone.** Proposals
|
|
301
|
+
`0034` (`evals` and `telemetry`) and `0038` (its spend keys) each say their keys ride the MINOR this key
|
|
302
|
+
needs and that neither waits for the other; none of them is drafted, and `0034`'s accepted text names no
|
|
303
|
+
version number, so they take the next free MINOR when they are drafted.
|
|
304
|
+
|
|
305
|
+
**Measured across the bump, as for 2.7 and 2.8:** `node cli/doctor.mjs .portulan examples` is GREEN, with
|
|
306
|
+
`.portulan` still on 2.8 and `examples/` on 2.4, both untouched. Neither declares the key, because
|
|
307
|
+
nothing measures the always tier yet and a budget nothing checks reads as a rail. What it costs:
|
|
308
|
+
`KNOWN_SPECS` in [`../cli/index.mjs`](../cli/index.mjs) and [`../cli/librarian.mjs`](../cli/librarian.mjs)
|
|
309
|
+
gains `"2.9"` by addition, and the four writers stay at `2.7`, since nothing they scaffold declares the
|
|
310
|
+
key.
|
|
311
|
+
|
|
206
312
|
**2.8 is a MINOR on the plainest terms in this train's history: one optional key and nothing else.**
|
|
207
313
|
`memory.store.budget.record_kilobytes` — the most bytes any single record may hold — joins the sibling
|
|
208
314
|
`kilobytes` it does not replace. Nothing is removed, renamed, tightened or defaulted, so every 2.7
|