@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
@@ -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.1.2",
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/main/docs/milestones/m04.md),
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
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "portulan": {
3
3
  "pack": "1.0",
4
- "version": "0.2.1"
4
+ "version": "0.3.0"
5
5
  },
6
6
 
7
7
  "name": "checkpoints",
@@ -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** — commit once the numbered adjustments are folded in.
69
- - **REQUEST-CHANGES** — the diff does not meet the criterion; it needs work and a second pre-commit pass.
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
- local_refs=0
39
+ exempt=0
73
40
 
74
41
  while IFS= read -r file; do
75
- # One `uses:` per line is the shape every workflow in the wild uses; a folded or flow-mapping
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
- # A local action lives in this repository and moves with it — there is no third-party
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
- # A container reference is pinned by digest, not by commit. Out of scope and named.
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: a git object name is hex and hex is case-insensitive, so an uppercase or
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 [ "$local_refs" -gt 0 ]; then
124
- printf ', %s local or container reference(s) exempt' "$local_refs"
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 nine**, and it counts what it always
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, and now stands at
154
- **nine**, is worth noticing rather than absorbing. Each is a genuine dependency between two keys and none
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.8** | the Workspace Definition — the manifest at a workspace root |
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.8**. It did **not** move when the Pack Definition arrived, because
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