@sleepy_panda_srl/portulan 0.1.1 → 0.1.3

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.
@@ -0,0 +1,134 @@
1
+ {
2
+ "portulan": {
3
+ "releaseEval": "1"
4
+ },
5
+ "version": "0.1.3",
6
+ "captured": "2026-09-16",
7
+ "source": {
8
+ "commit": "fac7b6d36ce3aa53ce93381ef8cce07e503608e4",
9
+ "clean": true
10
+ },
11
+ "host": {
12
+ "node": "v26.8.1",
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": "control-chars",
50
+ "exit": 0
51
+ },
52
+ {
53
+ "id": "rule-carriers",
54
+ "exit": 0
55
+ },
56
+ {
57
+ "id": "pack-version",
58
+ "exit": 0
59
+ },
60
+ {
61
+ "id": "pack-identity",
62
+ "exit": 0
63
+ },
64
+ {
65
+ "id": "payload",
66
+ "exit": 0
67
+ },
68
+ {
69
+ "id": "eval-bundle",
70
+ "exit": 0
71
+ },
72
+ {
73
+ "id": "goldens",
74
+ "exit": 0
75
+ },
76
+ {
77
+ "id": "mutants",
78
+ "exit": 0
79
+ },
80
+ {
81
+ "id": "fuzz-shell",
82
+ "exit": 0
83
+ },
84
+ {
85
+ "id": "version-carriers",
86
+ "exit": 0
87
+ },
88
+ {
89
+ "id": "skill-goldens",
90
+ "exit": 0
91
+ },
92
+ {
93
+ "id": "review-loop",
94
+ "exit": 0
95
+ },
96
+ {
97
+ "id": "telemetry",
98
+ "exit": 0
99
+ },
100
+ {
101
+ "id": "drills",
102
+ "exit": 0
103
+ },
104
+ {
105
+ "id": "ab",
106
+ "exit": 0
107
+ },
108
+ {
109
+ "id": "ab-grade",
110
+ "exit": 0
111
+ },
112
+ {
113
+ "id": "ab-run",
114
+ "exit": 0
115
+ },
116
+ {
117
+ "id": "tools/github:actions-pinned",
118
+ "exit": 0
119
+ }
120
+ ],
121
+ "excluded": [
122
+ {
123
+ "id": "release-eval",
124
+ "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."
125
+ }
126
+ ],
127
+ "abBaseline": {
128
+ "snapshot": "evals/ab/baseline.json",
129
+ "register": "evals/ab/baseline.md",
130
+ "captured": "2026-08-31T09:05:32.699Z",
131
+ "commit": "a642d551eec6c146c67ef0acfe3f8ba535c5ce6e",
132
+ "clean": false
133
+ }
134
+ }
@@ -0,0 +1,64 @@
1
+ # Eval result — Portulan 0.1.3
2
+
3
+ > **Generated. Do not edit.** Rendered from [`evals/releases/0.1.3.json`](0.1.3.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.1.3` |
10
+ | Captured | 2026-09-16 |
11
+ | Commit | `fac7b6d36ce3aa53ce93381ef8cce07e503608e4` |
12
+ | Tree at capture | clean |
13
+ | Node | `v26.8.1` |
14
+ | Platform | `darwin` |
15
+
16
+ ## The rails, and what each returned
17
+
18
+ 26 of the 27 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
+ | `control-chars` | green |
31
+ | `rule-carriers` | green |
32
+ | `pack-version` | green |
33
+ | `pack-identity` | green |
34
+ | `payload` | green |
35
+ | `eval-bundle` | green |
36
+ | `goldens` | green |
37
+ | `mutants` | green |
38
+ | `fuzz-shell` | green |
39
+ | `version-carriers` | green |
40
+ | `skill-goldens` | green |
41
+ | `review-loop` | green |
42
+ | `telemetry` | green |
43
+ | `drills` | green |
44
+ | `ab` | green |
45
+ | `ab-grade` | green |
46
+ | `ab-run` | green |
47
+ | `tools/github:actions-pinned` | green |
48
+
49
+ **`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.
50
+
51
+ ## The A/B baseline this release ships against
52
+
53
+ [`evals/ab/baseline.md`](../../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**.
54
+
55
+ **Its figures are not repeated here.** They have one carrier and this is not it.
56
+
57
+ ## What this record does not establish
58
+
59
+ - **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.
60
+ - **The recipes were not run at the tag.** They ran at `fac7b6d36ce3aa53ce93381ef8cce07e503608e4`, 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.
61
+ - **`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.
62
+ - **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.
63
+ - **The tree was clean at capture**, so the commit named above is what was measured.
64
+ - **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 argument,
4
+ the limits and the two designs a fresh context reversed are in [`../README.md`](../README.md); the
5
+ mechanism is [`../../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.1",
3
+ "version": "0.1.3",
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,18 @@
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/payload.mjs"
25
30
  ],
26
31
  "repository": {
27
32
  "type": "git",
package/packs/README.md CHANGED
@@ -15,18 +15,14 @@ array, in the canonical `category/name` form.
15
15
 
16
16
  ## Why there is a `.claude-plugin/plugin.json` in *this* directory
17
17
 
18
- Because **this directory is itself a plugin payload**, and it had been shipping as one while declaring
19
- nothing. The private feed's `portulan-checkpoints` entry is a `git-subdir` source rooted here, so what a
20
- host installs is the contents of `packs/` — and until 2026-08-09 that payload carried no plugin manifest
21
- at all. The host had nothing to read, registered nothing, and said so only as a count nobody was
22
- watching: **`claude plugin details portulan-checkpoints@portulan-internal` → `Skills (0)`**, for the
23
- whole of milestone 6 and after.
24
-
25
- That is [#134](https://github.com/sleepy-panda-srl/portulan/issues/134)'s `Skills (0)` bullet, and the
26
- issue's own diagnosis of it was wrong: it attributed the zero to a declared path pointing one level too
27
- high, which is a real trap and a different one. Measured on Claude Code 2.1.226 by installing this
28
- directory both ways — **`Skills (0)` without the manifest, `Skills (3)` with it**, the same three
29
- `SKILL.md` files either way.
18
+ Because **this directory is itself a plugin payload**. The private feed's
19
+ `portulan-checkpoints` entry is a `git-subdir` source rooted here, so what a host installs is the
20
+ contents of `packs/` — and a payload with no plugin manifest registers nothing, silently, reporting
21
+ it only as a count nobody is watching: `claude plugin details` → `Skills (0)`.
22
+
23
+ **Registration is a property of the plugin manifest and of nothing else.** Measured on Claude Code
24
+ 2.1.226 by installing this directory both ways — **`Skills (0)` without the manifest, `Skills (3)`
25
+ with it**, the same three `SKILL.md` files either way.
30
26
 
31
27
  The manifest declares `./rituals/checkpoints/skills/` — the directory that actually holds them — because
32
28
  **a host expands a declared skills path exactly one level and no further**. Declaring `./rituals/`
@@ -50,12 +46,6 @@ exists and the feed's pin moves to it. Until then the green is demonstrated on t
50
46
  for the feed — a distinction this repository would rather write down than round off.
51
47
 
52
48
  > **Two packs exist.** [`rituals/checkpoints`](rituals/checkpoints/README.md), the supervised-build
53
- > ritual, as of milestone 6; and [`tools/github`](tools/github/README.md), one verify recipe, authored
54
- > at milestone 7 session 5 because a demonstration needed a real subject. `stacks/` is still empty and
55
- > says why. _(This said "One pack exists … `stacks/` and `tools/` are still empty" from milestone 6
56
- > until 2026-08-13, and the second half went false the day `tools/github` landed beside it.)_
57
- >
58
- > This file read "Nothing here yet" for four milestones, and the pack it predicted — a cross-artifact
59
- > consistency check — is not the one that arrived. Recorded rather than quietly replaced: the first
60
- > pack was chosen by which ritual this project could show working evidence for, not by which was named
61
- > first.
49
+ > ritual; and [`tools/github`](tools/github/README.md), one verify recipe. `stacks/` is still empty and
50
+ > says why.
51
+
@@ -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:
@@ -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
 
@@ -3,10 +3,7 @@
3
3
  Tool packs — integrations for the tools and MCP servers an agent drives (issue trackers, cloud
4
4
  CLIs, design tools, databases), with the how-to and the gate classification for each.
5
5
 
6
- > **One tool pack exists** — [`github/`](github/README.md), since milestone 7 session 5. _(This read
7
- > "No tool pack exists yet" until 2026-08-13, beside the directory that refutes it.)_ What changed at
8
- > milestone 6 was the machinery rather
9
- > than this directory. Milestone 4 built the thing that would read one — a gate policy in a
6
+ > **One tool pack exists** — [`github/`](github/README.md). The machinery that reads one is a gate policy in a
10
7
  > host-neutral vocabulary, dispatched on by [`../../cli/compile.mjs`](../../cli/compile.mjs) — and the
11
8
  > cascade's missing middle (`core < pack < workspace`) is now bridged: a pack's `contributes.gates`
12
9
  > fragments are resolved and merged into the policy, **tighten-only**, by the same compiler.
package/spec/README.md CHANGED
@@ -6,8 +6,7 @@ what makes both machine-readable instead of a folder convention.
6
6
 
7
7
  - [`workspace.schema.json`](workspace.schema.json) — the **Workspace Definition**: the manifest at a
8
8
  workspace root. Normative.
9
- - [`pack.schema.json`](pack.schema.json) — the **Pack Definition**: the manifest at a pack root, added at
10
- milestone 6, declaring what the pack contributes to the cascade. Normative, and on **its own version
9
+ - [`pack.schema.json`](pack.schema.json) — the **Pack Definition**: the manifest at a pack root, declaring what the pack contributes to the cascade. Normative, and on **its own version
11
10
  train** — see [Versioning](#versioning-and-migrations).
12
11
  - [`slots.md`](slots.md) — every workspace slot: what it holds, why it exists, where it was derived from,
13
12
  and what `doctor` checks about it. The rationale lives there; this file is the orientation.
@@ -64,7 +63,7 @@ read.
64
63
  The fourth, **`pointer`**, added at 2.7, governs nothing. It names the workspace that governs this
65
64
  repository and carries no slots of its own — a repository whose policy layer resides elsewhere says so in
66
65
  one thin manifest instead of keeping a second copy nothing holds in agreement. That is the maintainer's
67
- residence ruling of 2026-07-30 in the schema
66
+ residence ruling in the schema
68
67
  ([proposal 0017](../.portulan/proposals/0017-one-repository-one-governing-workspace.md)):
69
68
 
70
69
  > A repository is governed by exactly one workspace. It carries its own full workspace, or a pointer to
@@ -76,11 +75,11 @@ makes that a property rather than a promise is that every feature keys to a work
76
75
  to a residence. Nothing in `doctor`, `compile` or `index` asks where the manifest lives. The proposal
77
76
  carries the one place where that was not yet true, and it was a discovery gap rather than a feature gap:
78
77
  the boot skill searched the project directory only, so a repository governed from a feed booted to a
79
- pointer's honest report rather than to a resolved workspace. **Closed at milestone 7** by
78
+ pointer's honest report rather than to a resolved workspace. **Closed** by
80
79
  [`../cli/discover.mjs`](../cli/discover.mjs), which reads the host's installed-plugin record and
81
80
  dereferences a pointer's `governed_by` — from disk, never over the network. The other half of the same amendment — the **pack** root
82
81
  ([#123](https://github.com/sleepy-panda-srl/portulan/issues/123)) — landed beside it: `--pack-root
83
- auto` resolves one from that same record. It ran **only when asked** until 2026-08-13, which was narrower
82
+ auto` resolves one from that same record. It ran **only when asked**, which was narrower
84
83
  than the row's *"optional where discovery finds a root"* and was recorded as a narrowing rather than read
85
84
  as compliance. **The narrowing is now lifted and the row's word is met**: discovery runs on the unasked
86
85
  path, and `auto` selects the strict degrade rather than unlocking anything. The narrowing's stated reason
@@ -159,10 +158,8 @@ real cost, stated here rather than discovered. The subset earns its narrowness b
159
158
  completely and honestly; the price is this list, and the list is the thing to watch if it keeps growing.
160
159
  **Four MINORs at +2 each** was the growth rate to hold the next bump against, and **2.3's, 2.5's and
161
160
  2.6's** pairs are the *same* pair three times over — a series needs a store and its index must sit outside
162
- it — which was an argument for generalising it rather than adding it again. _(2.4's pair is `librarian`'s
163
- and is a different one: a pass needs a store to age, and a proposal threshold needs a proposal series.
164
- An earlier draft of this sentence read "2.4's, 2.5's and 2.6's", which would have made the repeating pair
165
- four bumps old instead of three and misattributed `memory`'s contribution to `librarian`.)_
161
+ it — which was an argument for generalising it rather than adding it again. _(2.4's pair is `librarian`'s and is a
162
+ different one: a pass needs a store to age, and a proposal threshold needs a proposal series.)_
166
163
 
167
164
  **2.7 is the first MINOR since 2.2 that adds nothing to this list**, and that is the reason its pointer
168
165
  requirement went into the schema's `oneOf` instead. The generalisation the paragraph above kept asking for
@@ -198,7 +195,7 @@ number governing both would make a bump in either mean a change in the other:
198
195
  | Schema | Manifest key | Current | What it governs |
199
196
  |---|---|---|---|
200
197
  | [`workspace.schema.json`](workspace.schema.json) | `portulan.spec` | **2.8** | the Workspace Definition — the manifest at a workspace root |
201
- | [`pack.schema.json`](pack.schema.json) | `portulan.pack` | **1.0** | the Pack Definition — the manifest at a pack root, added at milestone 6 |
198
+ | [`pack.schema.json`](pack.schema.json) | `portulan.pack` | **1.0** | the Pack Definition — the manifest at a pack root. |
202
199
 
203
200
  The rules below apply to each train independently. `portulan.spec` is `MAJOR.MINOR`, and the current
204
201
  Workspace Definition version is **2.8**. It did **not** move when the Pack Definition arrived, because
@@ -300,7 +297,7 @@ better, and not a fix.)_
300
297
 
301
298
  This migration is a paragraph and a version bump, and a directory holding a document that says "add one
302
299
  line" would be scaffolding pretending to be machinery. This paragraph therefore ended **"There is still
303
- no `migrations/` directory: one arrives when a migration needs code"** — and at milestone 7 session 9
300
+ no `migrations/` directory: one arrives when a migration needs code"** — and
304
301
  one did, on that stated condition rather than because a tool wanted a home.
305
302
 
306
303
  **[`migrations/`](migrations/) is now the contract**, and its README is where a step is defined. What
@@ -328,24 +325,24 @@ workspace with no constitution slot and a green report.
328
325
  |---|---|---|
329
326
  | Every `.json` file parses | [`../.portulan/verify/json.sh`](../.portulan/verify/json.sh) | **Built.** Well-formedness only — it does not read the schema. |
330
327
  | Manifest conforms to the schema | [`../cli/doctor.mjs`](../cli/doctor.mjs) | **Built.** Names the violated constraint and its location. |
331
- | A declared pack **resolves** | `doctor` | **Built** (milestone 6). Roots are derived from `tree`, and named roots may be given with `--pack-root`, which **replace** the derived one rather than preceding it, so a from-a-feed demonstration cannot be satisfied by a copy in the local tree at all. A workspace with no `tree` and no named root has its packs reported *unverifiable* rather than failed. Resolution **from a feed is demonstrated** as of milestone 6, session 1: the checkpoint ritual pack installed from the private `portulan-internal` marketplace and resolved from the install root, which is the same code path the derived one takes. **Discovery landed at milestone 7** ([#123](https://github.com/sleepy-panda-srl/portulan/issues/123)): `--pack-root auto` reads the host's installed-plugin record — in **both** shapes a plugin lands in, packs under `packs/` for a repository-shaped one and categories at the install root for a flat one, which is what this project's own feed ships. **Amended 2026-08-12:** asked-for discovery is **unioned** with the tree-derived root, discovered first, so a workspace composing a cache-installed pack *and* one of its own resolves both — which no invocation could do before without naming a cache path by hand. The **named**-root property above is untouched: a named root still replaces the derived one, so a from-a-feed demonstration given one cannot be satisfied by a local copy. What the union trades is the weaker guarantee that `auto` *alone* implied provenance; it no longer does, and each pack's resolution states which root answered instead. Asking for a named root **and** `auto` together is refused (exit 2) rather than silently resolved to one of them. **Amended 2026-08-13 — the disposal:** discovery runs on the **unasked** path too, because the row's clause is that `--pack-root` is *optional where discovery finds a root* and it was not. `auto` now selects the strict degrade (asked-and-could-not-look is exit 2) rather than unlocking discovery. Two things bound it, and neither makes a bare run host-independent — `doctor` is a per-host capability report and a bare verdict does move with the machine. First, every **required** check **names** its root, which replaces every other source, so it cannot consult the host at all. Second, `doctor`'s note-vs-fail keying is on a root's **origin**, so discovery can turn a note into a **resolution** and never a **miss** into a failure — a miss under a root the workspace or the caller claimed stays a FAIL whatever is installed. It softens nothing: a discovered copy that resolves and is *invalid* still fails, with its origin named. |
332
- | A pack manifest conforms to the Pack Definition | `doctor` | **Built** (milestone 6). Same validator, same subset, a different schema and version train. |
333
- | A pack's skills and personas are opened and validated | `doctor` | **Built** (milestone 7 session 2), on the maintainer's ruling of 2026-08-03 that row 7's validation reads broad. A skill's frontmatter (`name` kebab-case, `description` non-empty — the two a host reads before loading a body); a persona against the five-part contract, **including the memory scope** `core/operating/memory.md` promised would be validated at this milestone; and the tier no role may claim. Containment is checked **after resolution**, because opening these paths is what made an escaping value stop being inert. Three honest limits, each measured rather than assumed. The skills walk stops **three levels down**, matching `cli/plugin-lint.mjs`'s bound so two walkers over the same key cannot disagree, and it **reports the directories it did not descend into** rather than counting a root it never opened as empty. The Prohibited check is a prose heuristic defeated by any negation **in the same block** — a bullet list counts as one block — and the structural fix is a declared reach *field*, a contract change that is not built. And the five-part check is **presence**, not content: a persona whose sections are all empty passes, so a declared memory scope is read and not honoured. |
334
- | Pack gate fragments merge **tighten-only** | [`../cli/compile.mjs`](../cli/compile.mjs) | **Built** (milestone 6). Two layers and two axes: `auto` is absent from the Pack Definition's tier enum, so a demotion to unattended is unexpressible; the comparison against the composed base is the compiler's, and both a weakened **tier** and a changed **action** throw rather than being dropped. The action half is the one a tier-only check misses — raising the tier while swapping the matcher removes the gate and still reads as a tightening. |
335
- | A pack declaring a version ahead of `doctor` | `doctor` | **Built** (milestone 6). Refused rather than graded, symmetric with the workspace train, and on the Pack Definition's own `$id`. |
328
+ | A declared pack **resolves** | `doctor` | **Built**. Roots are derived from `tree`, and named roots may be given with `--pack-root`, which **replace** the derived one rather than preceding it, so a from-a-feed demonstration cannot be satisfied by a copy in the local tree at all. A workspace with no `tree` and no named root has its packs reported *unverifiable* rather than failed. Resolution **from a feed is demonstrated**: the checkpoint ritual pack installed from the private `portulan-internal` marketplace and resolved from the install root, which is the same code path the derived one takes. **Discovery is built** ([#123](https://github.com/sleepy-panda-srl/portulan/issues/123)): `--pack-root auto` reads the host's installed-plugin record — in **both** shapes a plugin lands in, packs under `packs/` for a repository-shaped one and categories at the install root for a flat one, which is what this project's own feed ships. Asked-for discovery is **unioned** with the tree-derived root, discovered first, so a workspace composing a cache-installed pack *and* one of its own resolves both. The **named**-root property above is untouched: a named root still replaces the derived one, so a from-a-feed demonstration given one cannot be satisfied by a local copy. What the union trades is the weaker guarantee that `auto` *alone* implied provenance; it no longer does, and each pack's resolution states which root answered instead. Asking for a named root **and** `auto` together is refused (exit 2) rather than silently resolved to one of them. Discovery runs on the **unasked** path too, because `--pack-root` is *optional where discovery finds a root*. `auto` now selects the strict degrade (asked-and-could-not-look is exit 2) rather than unlocking discovery. Two things bound it, and neither makes a bare run host-independent — `doctor` is a per-host capability report and a bare verdict does move with the machine. First, every **required** check **names** its root, which replaces every other source, so it cannot consult the host at all. Second, `doctor`'s note-vs-fail keying is on a root's **origin**, so discovery can turn a note into a **resolution** and never a **miss** into a failure — a miss under a root the workspace or the caller claimed stays a FAIL whatever is installed. It softens nothing: a discovered copy that resolves and is *invalid* still fails, with its origin named. |
329
+ | A pack manifest conforms to the Pack Definition | `doctor` | **Built**. Same validator, same subset, a different schema and version train. |
330
+ | A pack's skills and personas are opened and validated | `doctor` | **Built**, on the maintainer's ruling that row 7's validation reads broad. A skill's frontmatter (`name` kebab-case, `description` non-empty — the two a host reads before loading a body); a persona against the five-part contract, **including the memory scope** `core/operating/memory.md` promised would be validated at this milestone; and the tier no role may claim. Containment is checked **after resolution**, because opening these paths is what made an escaping value stop being inert. Three honest limits, each measured rather than assumed. The skills walk stops **three levels down**, matching `cli/plugin-lint.mjs`'s bound so two walkers over the same key cannot disagree, and it **reports the directories it did not descend into** rather than counting a root it never opened as empty. The Prohibited check is a prose heuristic defeated by any negation **in the same block** — a bullet list counts as one block — and the structural fix is a declared reach *field*, a contract change that is not built. And the five-part check is **presence**, not content: a persona whose sections are all empty passes, so a declared memory scope is read and not honoured. |
331
+ | Pack gate fragments merge **tighten-only** | [`../cli/compile.mjs`](../cli/compile.mjs) | **Built**. Two layers and two axes: `auto` is absent from the Pack Definition's tier enum, so a demotion to unattended is unexpressible; the comparison against the composed base is the compiler's, and both a weakened **tier** and a changed **action** throw rather than being dropped. The action half is the one a tier-only check misses — raising the tier while swapping the matcher removes the gate and still reads as a tightening. |
332
+ | A pack declaring a version ahead of `doctor` | `doctor` | **Built**. Refused rather than graded, symmetric with the workspace train, and on the Pack Definition's own `$id`. |
336
333
  | Path slots resolve to real files | `doctor` | **Built.** File-versus-directory too; a slot resolving outside the workspace is reported, never failed. |
337
334
  | Workspace claims match the tree | `doctor` | **Built, where a tree is declared** — repo-card build/test/run lines and layout, and the gate map's required-check claim. A workspace with no `tree` has those claims *reported unverifiable*. |
338
335
  | A rule's provenance is well-formed | `doctor` | **Built.** On `type: rule` records in the `memory` slot; the form only, never the truth. |
339
336
  | Sealed proportion reported | `doctor` | **Built.** Over rules, which is the denominator the mandate uses. |
340
- | The recipes a workspace declares actually run | Stop-gate runner | **Built** (milestone 4), for the **default** recipe only, and outside `doctor` — which still reads recipes and executes none. A non-default recipe declaring a tool it does not have still passes. |
341
- | The gate policy compiles to host enforcement | `compile` | **Built** (milestone 4). Two backends — the Claude Code host and the GitHub repository ruleset that is the platform floor. Every rule ends as compiled or refused-with-a-reason, **per backend**, and each emitted artifact is held to the policy by a verify recipe. That the *host* honours it is not checkable here — CI installs nothing. |
342
- | What each backend cannot enforce | `doctor` · `compile --matrix` | **Built** (milestone 4). Per-backend coverage, the gates no backend compiles, and the floor's declared status checks against the tree's workflow jobs — the last of those a failure, the rest reports. Exported-versus-live drift is **not** checked: `doctor` does not fetch settings, and no recipe here makes a network call. |
337
+ | The recipes a workspace declares actually run | Stop-gate runner | **Built**, for the **default** recipe only, and outside `doctor` — which still reads recipes and executes none. A non-default recipe declaring a tool it does not have still passes. |
338
+ | The gate policy compiles to host enforcement | `compile` | **Built**. Two backends — the Claude Code host and the GitHub repository ruleset that is the platform floor. Every rule ends as compiled or refused-with-a-reason, **per backend**, and each emitted artifact is held to the policy by a verify recipe. That the *host* honours it is not checkable here — CI installs nothing. |
339
+ | What each backend cannot enforce | `doctor` · `compile --matrix` | **Built**. Per-backend coverage, the gates no backend compiles, and the floor's declared status checks against the tree's workflow jobs — the last of those a failure, the rest reports. Exported-versus-live drift is **not** checked: `doctor` does not fetch settings, and no recipe here makes a network call. |
343
340
  | One repository, one governing workspace | `doctor` | **Built** (2.7). Two refusals need only the manifest in hand: a `pointer` carrying governing slots, and a governing kind carrying a `governed_by`. Both print the ruling's own sentence, which is why they are `doctor`'s and not the schema's — a schema failure returns at *the manifest must conform first* and the sentence would never reach a reader. |
344
- | A repository named by one workspace and governed by another | `doctor --repo-root` | **Built** (2.7), **where visible**. Roots are named, never discovered — **and since milestone 7 this is the narrower limit of the two**: `--pack-root` gained `auto` and `--repo-root` did not, because what it would have to find is a *repository checkout*, which no host plugin record lists. The amendment that ordered discovery names the plugin cache in its own title, so this is outside its scope rather than owed by it. Without a root the check **reports that it did not run** rather than passing quietly. Visibility is one-way: the refusal runs from the naming workspace outward, so a repository carrying a full workspace cannot see a portfolio that claims it. A workspace that names its **own** repository finds itself and is not two managers; that identity is compared on the real path. |
345
- | A pointer resolves to the workspace it names | `discover` · `doctor` | **Built** (milestone 7). `governed_by` is dereferenced against the host's installed-plugin record — `<config>/plugins/installed_plugins.json`, `CLAUDE_CONFIG_DIR` overriding the config directory — and the match is on the **governing manifest's `name`**, never on a plugin's, with `governed_by.feed` constraining the marketplace where it is declared. Four answers, because three of them are not *no*: `resolved` (exactly one), `not-installed`, `ambiguous` (two or more — **refused and both named**, never ranked), `could-not-look` (a record that would not parse, which must never spend as absence). **Nothing is fetched**: the record is read from disk and no path here touches the network, so *discovery of anything not installed* stays out of scope as row 7 states. `doctor` **reports** the answer on a pointer and never grades it — a pointer whose governor is uninstalled is a correct pointer, and no discovery outcome moves this tool's exit code. The candidate locations inside a plugin payload are a **named pair** — `<installPath>/workspace.json` and `<installPath>/.portulan/workspace.json` — rather than a walk, which is what keeps this bundle's demo and drifted fixtures out of reach by construction — neither sits at one; the bundle's own `.portulan/` does, which is the correct answer to a pointer naming `portulan` rather than a hole. `init` **asks** the residence question and writes a pointer as of milestone 7 session 1; `vendor` **performs the switch in both directions** as of session 3, and both write pointers rather than resolving them. |
341
+ | A repository named by one workspace and governed by another | `doctor --repo-root` | **Built** (2.7), **where visible**. Roots are named, never discovered — **and this is the narrower limit of the two**: `--pack-root` gained `auto` and `--repo-root` did not, because what it would have to find is a *repository checkout*, which no host plugin record lists. The amendment that ordered discovery names the plugin cache in its own title, so this is outside its scope rather than owed by it. Without a root the check **reports that it did not run** rather than passing quietly. Visibility is one-way: the refusal runs from the naming workspace outward, so a repository carrying a full workspace cannot see a portfolio that claims it. A workspace that names its **own** repository finds itself and is not two managers; that identity is compared on the real path. |
342
+ | A pointer resolves to the workspace it names | `discover` · `doctor` | **Built**. `governed_by` is dereferenced against the host's installed-plugin record — `<config>/plugins/installed_plugins.json`, `CLAUDE_CONFIG_DIR` overriding the config directory — and the match is on the **governing manifest's `name`**, never on a plugin's, with `governed_by.feed` constraining the marketplace where it is declared. Four answers, because three of them are not *no*: `resolved` (exactly one), `not-installed`, `ambiguous` (two or more — **refused and both named**, never ranked), `could-not-look` (a record that would not parse, which must never spend as absence). **Nothing is fetched**: the record is read from disk and no path here touches the network, so *discovery of anything not installed* stays out of scope as row 7 states. `doctor` **reports** the answer on a pointer and never grades it — a pointer whose governor is uninstalled is a correct pointer, and no discovery outcome moves this tool's exit code. The candidate locations inside a plugin payload are a **named pair** — `<installPath>/workspace.json` and `<installPath>/.portulan/workspace.json` — rather than a walk, which is what keeps this bundle's demo and drifted fixtures out of reach by construction — neither sits at one; the bundle's own `.portulan/` does, which is the correct answer to a pointer naming `portulan` rather than a hole. `init` **asks** the residence question and writes a pointer; `vendor` **performs the switch in both directions** as of session 3, and both write pointers rather than resolving them. |
346
343
  | A feed-side repo card's claims against a repo-side tree | — | **Not built** — the cross-repo claims-lint gap, priced in [proposal 0017](../.portulan/proposals/0017-one-repository-one-governing-workspace.md). A portfolio workspace declares no `tree`, so its cards' build/test/run and layout claims are *reported unverifiable*; repo-side the tree is present and the cards are not. No single CI run sees both halves. In-session validation does work, and `--repo-root` narrows the governance question specifically — neither closes this. **Tracked as [#138](https://github.com/sleepy-panda-srl/portulan/issues/138)**, filed 2026-07-30 at the maintainer's instruction. This row states no *arrives-at*, unlike its neighbour below which says outright that it is not planned as a gate; which milestone owns it — milestone 9 has been proposed, since its criterion is the first that needs both halves green together — is **the maintainer's to decide**, and this row will name it once [`../docs/plan.md`](../docs/plan.md) does. Until then the honest state is *not built, tracked, undispositioned* rather than a milestone claimed here first. |
347
344
  | A rule's link resolves | — | Not built, and not planned as a gate: dereferencing needs the network, and a gate that fails for reasons unrelated to the change under test is worse than no gate. |
348
- | Agent-legibility scored | `doctor` | **Built** (milestone 7 session 7). **Seven** dimensions, each optional in this definition so it can genuinely be absent and none of them a restatement of a check that already fails — *executable verification* was an eighth for one checkpoint until that rule was applied to it, since form 0 of the `oneOf` above requires `verify` of every workspace the score can reach: recipes declaring `requires` · a machine-readable gate policy · a `dod` slot · a memory store with a generated index · a handoff series with a generated index · affordances reachable for every product (its own or the workspace-level default) · each of those documents naming what an agent must **not** assume. The `affordances` slot is the **named input, not the only one** — a score confined to it could not tell two workspaces apart. **It moves no exit code**: a measurement is not a verdict, and one that could fail a workspace would make conformance a function of prose volume. The last dimension is a **form** check over a named heading table, so a limits section with an empty body passes. |
345
+ | Agent-legibility scored | `doctor` | **Built**. **Seven** dimensions, each optional in this definition so it can genuinely be absent and none of them a restatement of a check that already fails — *executable verification* was an eighth for one checkpoint until that rule was applied to it, since form 0 of the `oneOf` above requires `verify` of every workspace the score can reach: recipes declaring `requires` · a machine-readable gate policy · a `dod` slot · a memory store with a generated index · a handoff series with a generated index · affordances reachable for every product (its own or the workspace-level default) · each of those documents naming what an agent must **not** assume. The `affordances` slot is the **named input, not the only one** — a score confined to it could not tell two workspaces apart. **It moves no exit code**: a measurement is not a verdict, and one that could fail a workspace would make conformance a function of prose volume. The last dimension is a **form** check over a named heading table, so a limits section with an empty body passes. |
349
346
 
350
347
  As of milestone 2 the schema is a rail rather than only a specification — but read the right-hand column
351
348
  rather than the middle one. `doctor` checks that a workspace's claims are **well-formed and resolvable**;
@@ -1,9 +1,8 @@
1
1
  # `spec/migrations/` — what a migration is
2
2
 
3
3
  > Part of the **Workspace Definition**. [`../README.md`](../README.md)'s *Versioning and migrations*
4
- > section is the contract this directory implements; it said *"There is still no `migrations/`
5
- > directory … **One arrives when a migration needs code**"* until milestone 7 session 9, and this
6
- > directory arrives on that stated condition rather than because a tool wanted a home. The runner is
4
+ > section is the contract this directory implements, and this directory exists on that section's own
5
+ > stated condition: a migration needed code. The runner is
7
6
  > [`../../cli/upgrade.mjs`](../../cli/upgrade.mjs).
8
7
 
9
8
  ## A step, and the two kinds
package/spec/slots.md CHANGED
@@ -345,9 +345,7 @@ has a product-layer slot, which is what the criterion requires; *instances* may
345
345
  **What `doctor` checks:** each `product` path resolves; each `affordances` path **that is present**
346
346
  resolves; each `repos` entry names a card in the `repos` slot. Per-product `affordances` is optional
347
347
  because a product may inherit the workspace-level default — so the check is conditional, and `doctor`
348
- *reports* a product that has neither its own nor an inherited one rather than failing it. (An earlier
349
- draft of this line said `doctor` checks `affordances` unconditionally, which would have made the spec
350
- demand something the schema declares optional; caught in review of the change that introduced it.) The
348
+ *reports* a product that has neither its own nor an inherited one rather than failing it. The
351
349
  claim it cannot check is whether the mission written there is still true.
352
350
 
353
351
  ## `affordances` — what the product offers an agent
@@ -465,12 +463,9 @@ workspace that cannot say which packs it composes leaves an agent to infer the m
465
463
  resolution order. The portfolio workspace at milestone 6 ships as a private-feed plugin composing a pack
466
464
  delivered by that feed — a manifest that cannot express that forces a MAJOR bump one milestone after v1.
467
465
 
468
- _(This sentence said **premium** packs until milestone 6, session 1, and no shipped pack is premium: the
469
- one the portfolio workspace composes is the universal checkpoint ritual, delivered by the private feed and
470
- authored in the public engine. The prediction was reasonable and turned out not to be what milestone 6
471
- built, so it is corrected to what the tree shows rather than left as a promise about a pack nobody wrote.
472
- Whether a premium pack should exist at all is a commercial question for the maintainer, not a gap this
473
- file may quietly keep open.)_
466
+ _(No shipped pack is premium: the one the portfolio workspace composes is the universal checkpoint
467
+ ritual, delivered by the private feed and authored in the public engine. Whether a premium pack should
468
+ exist at all is a commercial question for the maintainer, not a gap this file may quietly keep open.)_
474
469
 
475
470
  An array of names in cascade order, empty by default. Each name is the canonical `category/name` form
476
471
  the Pack Definition owns ([`pack.schema.json`](pack.schema.json)) — but the items stay **free strings**
@@ -776,8 +771,32 @@ Each of these was a candidate; none is an oversight.
776
771
  - **A per-host capability matrix.** The vision's `doctor` includes a per-host capability report, but that
777
772
  is generated from the host and the compiled gates, not declared by the team. It belongs with the
778
773
  enforcement backends in milestone 4.
779
- - **`evals`.** Milestone 8 owns the eval harness; a slot pointing at golden tasks before any exist would
780
- be the emptiest kind of scaffolding.
774
+ - **`evals`.** Milestone 8 owns the eval harness. **The original reason for deferring is spent, and the
775
+ deferral is re-argued rather than left standing on it.** It read *"a slot pointing at golden tasks
776
+ before any exist would be the emptiest kind of scaffolding"* — and golden tasks exist: the gate corpus
777
+ landed at clause (a) and the core-skill corpus at the row's original first subject. The premise
778
+ expired the day it stopped being true and nothing noticed, which is the shape this file elsewhere
779
+ calls a claim outliving its measurement.
780
+
781
+ **What the deferral now rests on.** A slot is the Workspace Definition's promise that every conforming
782
+ workspace can be asked one question and answer it. `evals/` today holds **two corpora with two
783
+ different oracles** — gates answered by the compiler's own `matchesRule`, skills answered by
784
+ mandate-to-artifact bindings — plus a metering register, and a slot naming that directory would
785
+ promise adopters a shape this repository has not settled for itself. **Revisit when milestone 8
786
+ closes**, by which point the row's nine clauses will have decided what an eval artifact is here.
787
+ Filling it is a schema change, a spec version bump and a migration, so it is not a thing to do in
788
+ passing.
789
+
790
+ **Milestone 8 closed 2026-09-09, so this deferral is now DUE and is not discharged here.** The
791
+ close names it rather than filling it, which is what the sentence above asks for: the drafting is
792
+ commissioned by proposal
793
+ [`../.portulan/proposals/0034-one-spec-bump-carries-both-evals-and-telemetry.md`](../.portulan/proposals/0034-one-spec-bump-carries-both-evals-and-telemetry.md)
794
+ — accepted 2026-08-28 — which carries `evals` **and** `telemetry` in **one** MINOR and settles none of
795
+ the shape: the key names, either slot's structure, and whether a migration is owed are the drafted
796
+ work. Doing that drafting inside the close would be the *"splitting on speculation"* this entry warns
797
+ against, in the change that closed the row it was waiting for. **What the close settled is the
798
+ precondition, not the slot:** `evals/` now holds two corpora, a metering register, an A/B capture and
799
+ a per-release record layer, which is the shape a slot would have to name.
781
800
 
782
801
  ## What v2.0 is not
783
802