@sleepy_panda_srl/portulan 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/cli/README.md +145 -358
- package/cli/advisory.mjs +354 -0
- package/cli/comments.mjs +932 -0
- package/cli/compile.mjs +1719 -1699
- package/cli/context.mjs +883 -0
- package/cli/control-chars.mjs +12 -301
- package/cli/discover.mjs +17 -574
- package/cli/doctor.mjs +269 -1080
- package/cli/drills.mjs +92 -504
- package/cli/feedback.mjs +16 -237
- package/cli/finish.mjs +437 -0
- package/cli/form.mjs +644 -0
- package/cli/fuzz-shell.mjs +20 -305
- package/cli/gate.mjs +7 -199
- package/cli/goldens.mjs +26 -184
- package/cli/index.mjs +305 -655
- package/cli/init.mjs +243 -566
- package/cli/inside.mjs +3 -28
- package/cli/instructions.mjs +829 -0
- package/cli/ledger.mjs +882 -0
- package/cli/librarian.mjs +135 -599
- package/cli/manifest.mjs +2 -39
- package/cli/mutants.mjs +9 -170
- package/cli/new.mjs +12 -243
- package/cli/pack-identity.mjs +5 -74
- package/cli/pack-version.mjs +13 -255
- package/cli/plugin-lint.mjs +42 -658
- package/cli/portulan.mjs +6 -125
- package/cli/recipe-set.mjs +15 -245
- package/cli/release-eval.mjs +28 -348
- package/cli/review-meter.mjs +29 -265
- package/cli/rule-carriers.mjs +12 -175
- package/cli/sessions.mjs +82 -0
- package/cli/skill-goldens.mjs +10 -176
- package/cli/skills-set.mjs +12 -414
- package/cli/stop-gate.mjs +83 -514
- package/cli/symbols.mjs +1132 -0
- package/cli/telemetry.mjs +82 -505
- package/cli/upgrade.mjs +171 -314
- package/cli/vendor.mjs +146 -476
- package/cli/version-carriers.mjs +8 -86
- package/core/README.md +1 -1
- package/core/engine.md +3 -9
- package/core/operating/autonomy.md +1 -1
- package/core/operating/context.md +258 -0
- package/core/operating/evolution.md +3 -3
- package/core/operating/loop.md +13 -20
- package/core/operating/memory.md +12 -3
- package/core/operating/sessions.md +100 -0
- package/core/personas/README.md +33 -0
- package/core/personas/librarian.md +2 -2
- package/core/skills/clarify/SKILL.md +1 -1
- package/core/skills/consolidate/SKILL.md +5 -3
- package/core/templates/handoff.md +28 -15
- package/core/templates/memory-entry.md +1 -0
- package/evals/README.md +271 -845
- package/evals/releases/0.2.0.json +146 -0
- package/evals/releases/0.2.0.md +67 -0
- package/evals/releases/0.3.0.json +150 -0
- package/evals/releases/0.3.0.md +68 -0
- package/evals/releases/README.md +3 -3
- package/package.json +5 -2
- package/packs/.claude-plugin/plugin.json +2 -1
- package/packs/README.md +7 -4
- package/packs/rituals/checkpoints/README.md +1 -1
- package/packs/tools/README.md +8 -8
- package/packs/tools/github/verify/actions-pinned.sh +10 -55
- package/packs/tools/local-llm/README.md +136 -0
- package/packs/tools/local-llm/pack.json +32 -0
- package/packs/tools/local-llm/personas/local-worker.md +44 -0
- package/packs/tools/local-llm/runner/local-llm.mjs +1665 -0
- package/packs/tools/local-llm/skills/delegate-local/SKILL.md +64 -0
- package/packs/tools/local-llm/verify/README.md +18 -0
- package/spec/README.md +113 -6
- package/spec/migrations/0001-repository-declares-its-tree.mjs +1 -26
- package/spec/migrations/0002-bundle-fallback-path.mjs +13 -74
- package/spec/migrations/0003-handoff-index-not-kept.mjs +66 -0
- package/spec/migrations/0004-changelog-fragments.mjs +53 -0
- package/spec/migrations/0005-session-log-retired.mjs +69 -0
- package/spec/migrations/0006-boot-card.mjs +77 -0
- package/spec/migrations/0007-guidance-compiled.mjs +31 -0
- package/spec/migrations/0008-card-reading.mjs +55 -0
- package/spec/migrations/0009-instruction-sections-on-read.mjs +60 -0
- package/spec/migrations/0010-comment-rail.mjs +57 -0
- package/spec/migrations/README.md +45 -10
- package/spec/pack.schema.json +1 -1
- package/spec/slots.md +343 -27
- package/spec/workspace.schema.json +145 -4
package/cli/README.md
CHANGED
|
@@ -1,375 +1,162 @@
|
|
|
1
1
|
# cli/
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
`
|
|
5
|
-
enforcement) · `vendor` (materialise a workspace where it is needed — into a host, and the reverse) ·
|
|
6
|
-
`index` · `upgrade` · `new` · `feedback`. The entry point is
|
|
7
|
-
[`portulan.mjs`](portulan.mjs), reached as `portulan <subcommand>` through the `bin` in the
|
|
8
|
-
repository's `package.json`.
|
|
3
|
+
<!-- Generated by `node cli/roster.mjs --write` from the files it lists: never edit it by hand. The
|
|
4
|
+
`tests` recipe is red while it differs from that render (cli-roster.live.test.mjs). -->
|
|
9
5
|
|
|
10
|
-
|
|
11
|
-
`
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
ships is narrower than the tree and is not restated here as a number — `npm pack --dry-run` derives
|
|
17
|
-
it, and a hand-copied count is the thing that keeps going wrong in this file.)_
|
|
6
|
+
The command line of the npm package. `portulan <subcommand>` reaches each subcommand through
|
|
7
|
+
`package.json`'s `bin`, [`portulan.mjs`](portulan.mjs); every tool here also runs from a clone as
|
|
8
|
+
`node cli/<file>.mjs`. A subcommand's line is its `portulan --help` summary, and every other line is the
|
|
9
|
+
first paragraph of its file's header comment, which says what the file is. The groups come from the
|
|
10
|
+
code: `SUBCOMMANDS` in [`portulan.mjs`](portulan.mjs), `HOOK_RUNNERS` in [`compile.mjs`](compile.mjs),
|
|
11
|
+
and `EXCLUDED` in [`payload.mjs`](payload.mjs), which says why each file marked *Not shipped* stays home.
|
|
18
12
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`npx` user runs does not travel. The suite and its fixtures went first, then `eval-bundle` and its licence
|
|
23
|
-
template, on the eval payload's own reasoning — an evaluee receives the stamped licence, never the
|
|
24
|
-
stamp press. `ab`, `ab-run` and `ab-grade` joined them on 2026-09-01.
|
|
25
|
-
|
|
26
|
-
**The argument for the A/B trio is that the rig has one subject, and it is not the reader's
|
|
27
|
-
repository.** `ab.mjs`'s `DISPOSITIONS` classifies *this* workspace's `.portulan/` path by path, and
|
|
28
|
-
the table is compiled into the module rather than read from beside it: pointed at an adopter's
|
|
29
|
-
workspace `--check` exits 2 and names every disposition that matched nothing. There is no
|
|
30
|
-
configuration under which the trio does a consumer's tree any good — `ab-run`'s `SNAPSHOT` is one
|
|
31
|
-
baseline, of one arm, on one date, and `ab.mjs` refuses to build an arm it cannot classify entirely.
|
|
32
|
-
|
|
33
|
-
_**Not** because their registers resolve under `evals/`, which contributes nothing to the payload.
|
|
34
|
-
That reasoning is already ruled out in this tree, and the exclusion holds **despite** it rather than
|
|
35
|
-
because of it:_ [`eval-bundle.mjs`](eval-bundle.mjs)'s `EXCLUDED_TOP_LEVEL.evals` settled on 2026-08-24
|
|
36
|
-
that **the tool is product and the policy it reads is this team's** — `goldens` ships without its
|
|
37
|
-
corpus the way `compile` ships without `gates.json`. That distinction carries the whole load here:
|
|
38
|
-
`readCorpus(repoRoot, dir = CORPUS_DIR)` takes its directory as an argument, so an adopter can point it
|
|
39
|
-
somewhere; `ab.mjs`'s disposition table takes nothing and can be pointed nowhere.
|
|
40
|
-
|
|
41
|
-
_**The exclusion is narrower than the criterion, and every module is now classified rather than
|
|
42
|
-
merely counted.** The transitive import closure of [`portulan.mjs`](portulan.mjs) and its eight
|
|
43
|
-
subcommands, less the two compiled-hook runners, leaves **fourteen** shipped modules reachable from
|
|
44
|
-
nothing the package exposes. **Thirteen are `unruled`** — `control-chars`, `drills`, `fuzz-shell`,
|
|
45
|
-
`goldens`, `librarian`, `mutants`, `pack-identity`, `pack-version`, `review-meter`, `rule-carriers`,
|
|
46
|
-
`skill-goldens`, `telemetry` and `version-carriers` — most of them the `goldens` shape the 2026-08-24
|
|
47
|
-
rule keeps, none put the question the A/B trio was, and whether any follows is the maintainer's call,
|
|
48
|
-
the same sentence this file already makes about a ninth subcommand. **The fourteenth, `release-eval`,
|
|
49
|
-
is `product`**: ruled to ship on 2026-09-01, which is what an answered one looks like beside thirteen
|
|
50
|
-
unanswered.
|
|
51
|
-
|
|
52
|
-
**The sentence that stood here is now false and is corrected rather than deleted**: it said *nothing
|
|
53
|
-
derives payload membership*, which was true when it was written and stopped being true one pull request
|
|
54
|
-
later. [`payload.mjs`](payload.mjs) classifies every shipped `cli/*.mjs` and the `payload` recipe holds
|
|
55
|
-
it on every commit. What remains true is the narrower thing: `pack-identity` checks the packed
|
|
56
|
-
**bytes** and is silent on the **roster**, which is why a module could join the tarball under a green —
|
|
57
|
-
measured, by staging one and watching it. The `!` lines are still hand-maintained, but they are no
|
|
58
|
-
longer unchecked._
|
|
59
|
-
|
|
60
|
-
**All eight dispatch.** The entry point calls each one's exported `run` and returns the code
|
|
61
|
-
unchanged.
|
|
62
|
-
|
|
63
|
-
_A subcommand may be named by `docs/vision.md` before the tree carries it, and while that holds the
|
|
64
|
-
honest answer is to **list it and refuse** — exit 2, naming where it arrives. A stub exiting 0 would be
|
|
65
|
-
a fail-open where a user is most likely to trust silence. `portulan.mjs` carries that machinery
|
|
66
|
-
(`module: null` plus `arrives`) and its suite exercises the refusal on a synthetic entry, nothing
|
|
67
|
-
real being unbuilt today._
|
|
68
|
-
|
|
69
|
-
**Why eight.** `docs/vision.md` names all eight and is human-owned, so the roster of subcommands is
|
|
70
|
-
the constitution's to set and not an implementer's. A subcommand can be licensed by a milestone row
|
|
71
|
-
before the constitution catches up; while that holds, the tool says which document names it.
|
|
72
|
-
|
|
73
|
-
<!-- roster:begin -->
|
|
74
|
-
<!-- THE PARTITION BELOW IS RAILED. `cli-roster.live.test.mjs` reads the bare backticked names
|
|
75
|
-
between these two markers and holds them against the tracked NON-TEST `cli/*.mjs` modules, in
|
|
76
|
-
both directions — a test file belongs in the table below, never in this block: a
|
|
77
|
-
file classified by none of the three — this prose, `portulan.mjs`'s `SUBCOMMANDS`, or
|
|
78
|
-
`portulan.mjs` itself as the entry point — fails, and a name here matching no file fails.
|
|
79
|
-
Rewrite the prose freely — the markers are the anchor, not the sentences. A name may leave
|
|
80
|
-
only when the file does. **No count is written here**, for the reason this file gives about
|
|
81
|
-
every other hand-maintained figure. -->
|
|
82
|
-
Beside those eight sit the runnable tools that are on none of the lists — `plugin-lint`,
|
|
83
|
-
`librarian`, `control-chars`, `discover`, `rule-carriers`, `pack-version`, `eval-bundle`,
|
|
84
|
-
`pack-identity`, `version-carriers`, `goldens`, `skill-goldens`, `mutants`, `fuzz-shell`, `drills`, `review-meter`, `telemetry`, `payload`, `ab`, `ab-grade`, `ab-run` and `release-eval`. Each exists because a verify recipe or a
|
|
85
|
-
packaging check needed it; none is reachable as `portulan <subcommand>`.
|
|
86
|
-
The remaining files in this directory are neither subcommands nor tools: `gate` and `stop-gate` are
|
|
87
|
-
the **compiled-hook runners**, invoked by generated host configuration rather than by a person, and
|
|
88
|
-
`recipe-set`, `skills-set`, `manifest` and `inside` are modules other tools import — the first two
|
|
89
|
-
runnable in their own right, each printing the set it carries, and `inside` a single predicate that
|
|
90
|
-
lives alone so two large modules can both use it without importing each other.
|
|
91
|
-
<!-- roster:end -->
|
|
92
|
-
|
|
93
|
-
_(Those sentences carried hand-maintained counts, and the counts went wrong five times before
|
|
94
|
-
[#204](https://github.com/sleepy-panda-srl/portulan/issues/204) retired them — each correction right
|
|
95
|
-
on the day it landed, each stale within weeks. That is the argument for deriving the membership
|
|
96
|
-
rather than correcting it once more, which is
|
|
97
|
-
[#187](https://github.com/sleepy-panda-srl/portulan/issues/187)'s subject.)_
|
|
98
|
-
|
|
99
|
-
Being off the list is a fact about `docs/vision.md`, which names these eight subcommands and is
|
|
100
|
-
human-owned: whether any of the others ever joins them is the maintainer's call and not an implementer's,
|
|
101
|
-
so none is described here as *coming to the CLI* — and none is wired behind the entry point, which is
|
|
102
|
-
the same rule expressed in code rather than in a sentence.
|
|
103
|
-
|
|
104
|
-
**This paragraph is the one carrier of that roster**, and `portulan.mjs` and the root
|
|
105
|
-
[`../README.md`](../README.md) both cite it rather than repeating it. One rule with three carriers is
|
|
106
|
-
obeyed at the narrowest — the repair
|
|
107
|
-
[`0020`](../.portulan/proposals/0020-a-fix-is-not-done-at-the-site-it-was-found.md) prescribes where
|
|
108
|
-
a rule cannot be a function: state it once, and let the others cite.
|
|
109
|
-
|
|
110
|
-
The table below is checked against this directory by
|
|
111
|
-
[`../.portulan/verify/docs.sh`](../.portulan/verify/docs.sh) in **both directions**, on every run:
|
|
112
|
-
every file here has a row, and every row names a file that exists — save two, declared to the rail
|
|
113
|
-
rather than left as quiet exceptions.
|
|
114
|
-
|
|
115
|
-
**Two exceptions are deliberate and are declared to the rail rather than worked around it.** This
|
|
116
|
-
`README.md` has no row, because a file does not list itself; `fixtures/` has a row and is a directory
|
|
117
|
-
rather than a file, because what the table owes a reader there is one sentence about the whole
|
|
118
|
-
directory. A rail with an undeclared exception teaches the next person to widen the pattern until it
|
|
119
|
-
stops complaining, so both are named in the recipe and either one disappearing is itself a failure.
|
|
13
|
+
A tool belongs here when it works on any workspace or plugin root. This repository's own recipes live
|
|
14
|
+
in [`../.portulan/verify/`](../.portulan/verify/), and its operator tooling in
|
|
15
|
+
[`../.portulan/tools/`](../.portulan/tools/).
|
|
120
16
|
|
|
121
17
|
## What is here today
|
|
122
18
|
|
|
19
|
+
### The entry point and its subcommands
|
|
20
|
+
|
|
123
21
|
| File | What it is |
|
|
124
22
|
|---|---|
|
|
125
|
-
| [`portulan.mjs`](portulan.mjs) |
|
|
126
|
-
| [`
|
|
127
|
-
| [`
|
|
128
|
-
| [`
|
|
129
|
-
| [`
|
|
130
|
-
| [`
|
|
131
|
-
| [`
|
|
132
|
-
| [`
|
|
133
|
-
| [`
|
|
134
|
-
| [`plugin-lint.mjs`](plugin-lint.mjs) | The packaging validator: the plugin and marketplace manifests, the skills they declare, and the agents at `./agents/` that nothing declares. It also holds **`core/personas/` and `agents/` to each other** — both directions fail, because a persona this bundle ships and does not bind is doctrine the host never registers, and a binding whose persona is gone registers a role with no charter. It is the one location a host loads agents from, exported here as `AGENT_DIR` and imported by `doctor` rather than spelled twice. |
|
|
135
|
-
| [`plugin-lint.test.mjs`](plugin-lint.test.mjs) | Its test suite, likewise written first. |
|
|
136
|
-
| [`skills-set.mjs`](skills-set.mjs) | **The registrable set**: the one carrier of *what a plugin manifest must declare so a composed pack's skills register*. It reads each composed pack's `contributes.skills` and derives the paths, relativised to the plugin root — `--check` reports drift (1) and `--write` derives the key rather than leaving it hand-typed. Row 7 clause (b)'s adopter half, [#184](https://github.com/sleepy-panda-srl/portulan/issues/184): until it landed, registration was a property of `.claude-plugin/plugin.json` **alone**, so a composed pack's skill was invocable by coincidence of a path someone typed. It holds `HOST_SKILL_DEPTH` — the platform fact the derivation rests on, measured in `plugin-lint` and moved here when that file became a consumer. **Deliberately not a ninth `portulan` subcommand**, on `discover`'s precedent above. |
|
|
137
|
-
| [`skills-set.test.mjs`](skills-set.test.mjs) | Its test suite, written first. Every refusal has a sibling in `recipe-set.test.mjs`, because those were found by review rather than by design and a second carrier of the same shape should not have to rediscover them. |
|
|
138
|
-
| [`skills-set.live.test.mjs`](skills-set.live.test.mjs) | The same derivation against **this** repository rather than against fixtures — the guard for *a harness you write to check your own change inherits your change's blind spot*, which this project has now measured seven times, two of them in the session that added this file. Its load-bearing assertion is that the derived set equals what `.claude-plugin/plugin.json` already declares. |
|
|
139
|
-
| [`new.mjs`](new.mjs) | **The authoring subcommand**: it scaffolds a skill · persona · pack · workspace · gate-policy · repo-card from a core template **into a layer you own, never into `core/`**. The refusal is resolution-based rather than pattern-based, because every interesting escape — `packs/../core`, a symlinked destination — parses fine and only fails once resolved. Two codes, `0` wrote and `2` could not run: it renders no verdict, so it has no red. |
|
|
140
|
-
| [`new.test.mjs`](new.test.mjs) | Its test suite, written first. It establishes *never into `core/`* against the filesystem rather than against an argument check, and runs the real `doctor` and `plugin-lint` over what was scaffolded — a scaffold nothing validates is one nobody can trust. |
|
|
141
|
-
| [`vendor.mjs`](vendor.mjs) | **Materialises a workspace where it is needed, in either direction** — the constitution's gloss covers both. `--host` writes a self-contained `AGENTS.md` — core's kernel inlined, the workspace's slots named, packs named-not-composed — beside a copied `.portulan/`, for a host that cannot install the plugin. `--switch` changes residence, feed-side ↔ in-repo, under proposal [`0017`](../.portulan/proposals/0017-one-repository-one-governing-workspace.md): materialise at the new residence, leave a pointer or nothing at the old, `doctor` green at **both** ends before the old one is retired. Every handled failure leaves exactly one governing workspace; the residence is **never inferred** from a path, which is `init`'s rule for `init`'s reason. |
|
|
142
|
-
| [`vendor.test.mjs`](vendor.test.mjs) | Its test suite, written first. The group that matters *forces* a failure after each named write step, because the property this tool exists to protect is what a failure partway leaves on disk — an ordering nothing can interrupt is an ordering nobody has checked. Every end state is graded by the real `doctor`, never by a second opinion about what valid means. |
|
|
143
|
-
| [`collisions.test.mjs`](collisions.test.mjs) | **The collision contract — one rule, three carriers, pinned together.** Three tools here write into a tree somebody else owns, and all three answer the same question first: *is something already at the path I am about to write?* [`init.mjs`](init.mjs), [`new.mjs`](new.mjs) and [`vendor.mjs`](vendor.mjs) each export a `collisions()` that answers it with a different signature, a different return shape and a different refusal vocabulary, because each was copied from the last and adapted. This suite pins the three to the one question rather than to one another's spellings. |
|
|
144
|
-
| [`upgrade.mjs`](upgrade.mjs) | **The migration runner**, the last of the eight to be built. It reads [`../spec/migrations/`](../spec/migrations/) — the chain lives in the **spec**, because a migration is part of the Workspace Definition rather than part of a tool — and applies what a workspace owes. **Two kinds of step**, on the maintainer's ruling of 2026-08-12: a `version` step migrates a MAJOR, and a `repair` fixes what a rewriter owes a workspace it touched. Owedness is derived from the workspace's **state**, never from a stamp, so every step is idempotent and an interrupted run is recovered by re-running. It refuses in **both** directions: a workspace ahead of this bundle is could-not-run (upgrade the CLI, not the workspace), and one behind by a MAJOR that no step reaches is could-not-run too — either would otherwise plan to nothing and exit 0. `--write` applies, then grades with the real `doctor` and **rolls back on a red**, reporting what it could not put back rather than claiming the tree is clean. |
|
|
145
|
-
| [`upgrade.test.mjs`](upgrade.test.mjs) | Its test suite, written first, against `ws` views built in memory — the honest way to force a step's own branches red. Every case in it was **forced red by mutating the code it guards** before being trusted; the pass that did so found a guard with no test at all, where the arithmetic was covered and the tool's response to it was not. |
|
|
146
|
-
| [`upgrade.live.test.mjs`](upgrade.live.test.mjs) | The other half, and the one that matters: workspaces the **real `init`** drafts, from a real second bundle that is then deleted, with the drafted rail **run** before and after — exit 2 with its bundle gone, a verdict once repaired. Behaviour, not a string comparison on a script. It also pins that `.portulan` and `examples/` owe nothing, so `examples/` staying four MINORs back at 2.4 cannot be quietly restamped. Written because running the step over `.portulan` found what no fixture could: a handoff that *documents* the marker was read as a malformed marked line. |
|
|
147
|
-
| [`feedback.mjs`](feedback.mjs) | **The inbound half**: it files an issue from a report the user previewed, seam-scanned before it leaves the machine, under the Gated tier. Three verbs — `draft` writes a report into the workspace's `feedback/` directory, `preview` prints the exact bytes, `send --approve` files them through the user's own `gh` login. The payload is assembled from a **closed list** rather than filtered, so nothing here reads a workspace name, a repo card, a gate map, memory, a remote or a path. It ships **no seam terms** — the list is the adopter's and is looked for at `--seam-terms`, `$PORTULAN_SEAM_TERMS`, then `<workspace>/seam-terms.txt`; a hit refuses the send with **1**, a named list that cannot be read refuses it with **2**, and no list at all is *stated in the sentence the user approves* rather than passed over. Approval is per send and is never inherited from a draft or a preview. |
|
|
148
|
-
| [`feedback.test.mjs`](feedback.test.mjs) | Its test suite, written first, against `.portulan/tasks/0012-a-feedback-pipe-points-out-of-the-seam.md`'s *Done when* list. Nothing here reaches the network: `gh` arrives injected. The one property injection cannot prove — that the approved bytes are the sent bytes — is not proven by comparison but held by construction, and the suite asserts the construction: the previewed body **is** `payload()`'s return value, not a second rendering that agrees with it today. |
|
|
149
|
-
| [`feedback.live.test.mjs`](feedback.live.test.mjs) | The rail on the pair that cannot be collapsed. `package.json`'s `files` does not ship `.github/ISSUE_TEMPLATE/`, so the sender must carry the forms' field ids, labels, required flags, dropdown options and acknowledgement texts — one fact, two carriers, and the repair is `0020`'s: one carrier plus a rail. This reads the real forms and fails when the two disagree. Its own instrument is guarded first: if the reader stops understanding a form and returns nothing, the test goes **red** rather than vacuously green. |
|
|
150
|
-
| [`compile.mjs`](compile.mjs) | The enforcement compiler: a workspace's gate policy becomes host enforcement. Two backends — Claude Code `permissions` + `hooks`, and a GitHub repository ruleset for the platform floor — and the vocabulary it reads stays the workspace's, so a third backend translates the same policy instead of forcing it to be rewritten. `--workspace` takes a repository root **or the workspace directory itself**, which is how a feed-side workspace is reachable at all: it keyed on `.portulan` until the parity demonstration found it exiting 2 on the feed-side end of a switch. |
|
|
151
|
-
| [`compile.test.mjs`](compile.test.mjs) | Its test suite, likewise written first. Emission fidelity only — nothing in here can establish that a host *honours* what the compiler emits, which is a fact about a running host. |
|
|
152
|
-
| [`inside.mjs`](inside.mjs) | **Is one path inside another** — one predicate, one file, no imports. It began in [`index.mjs`](index.mjs) and moved here when [`compile.mjs`](compile.mjs) needed it too: taking it from there would have closed an import cycle, and this repository has already had one exit **13** in silence. Copying it instead is what its own docblock forbids — two copies of this rule drifted into the identical defect before either shipped. `index.mjs` re-exports it, so every existing importer is untouched. |
|
|
153
|
-
| [`index.mjs`](index.mjs) | The index generator, over **two** series since Workspace Definition 2.5: the memory store, which is size-budgeted so a breach is a red, and the handoff series, which is not — an append-only series has no consolidation to offer, so every remedy a budget could ask for is already barred. Every field on every line is derived from what it points at, so neither file has a hand-maintained half; the *carriers* differ per series and on evidence, a record's title being its filename and a handoff's its H1. It writes an over-budget index rather than refusing to — the remedy is consolidation, and consolidating needs the artifact to consolidate from. |
|
|
154
|
-
| [`index.test.mjs`](index.test.mjs) | Its test suite, written first. Derivation, drift and cost only — nothing in here can establish that the index is any good at *recall*, which is an eval question. |
|
|
155
|
-
| [`gate.mjs`](gate.mjs) | **The PreToolUse gate runner** — the *explanation* half of the enforcement compiler, wired by [`compile.mjs`](compile.mjs) into `.claude/settings.json`. It reads the host's hook payload on stdin, finds which rule of the policy the workspace **yields** the attempted action matches, and returns that rule's own sentence as the decision reason. *Yields*, not *declares*: [`../.portulan/gates.json`](../.portulan/gates.json) **plus the fragments its packs contribute**, composed through `compile`'s own `packContributions`/`composeFragments` so there is no second composer to drift. Where two rules match one call it answers with the **strongest** tier, not the first listed — composition makes that systematic, since added rules are appended and so always lose a first-match tie, but a single-file policy listing the weaker rule first diverged too. Strengthen-only: `ask` may become `deny`, never the reverse. **This file is not the gate** — the host's own matcher is; this says *why*, so a refusal arrives as a reason rather than a wall. |
|
|
156
|
-
| [`gate.test.mjs`](gate.test.mjs) | Its test suite, and the exception to "written first" for the same reason [`stop-gate.test.mjs`](stop-gate.test.mjs) is: it arrived with [#269](https://github.com/sleepy-panda-srl/portulan/issues/269), on a runner nothing had tested directly. **Every case spawns the real binary** with a payload on stdin, because the defect it closes was never in `decide` — it was in which rules reached `decide`, and a unit test would have passed on every day the hook was blind. |
|
|
157
|
-
| [`stop-gate.mjs`](stop-gate.mjs) | **The Stop-gate runner** — *done* is not a thing an agent may simply declare. Wired by [`compile.mjs`](compile.mjs) into `.claude/settings.json` as a `Stop` hook: when an agent tries to end its turn, this runs the workspace's default verify recipe and blocks the ending if the recipe is not green. [`../core/operating/verification.md`](../core/operating/verification.md) specifies the contract; this is that runner. |
|
|
158
|
-
| [`stop-gate.test.mjs`](stop-gate.test.mjs) | The exception to "written first": it covers the Stop-gate runner ([`stop-gate.mjs`](stop-gate.mjs)), and it exists because a supervisor found a fail-open and a forever-block in a runner nothing tested at all. Its cap arithmetic and date handling, and — since [#220](https://github.com/sleepy-panda-srl/portulan/issues/220) — the **did-work signals**, which spawn the real binary against real git fixtures for the reason [`gate.test.mjs`](gate.test.mjs) gives: the defect was that real git in a rebase-merging repository behaves otherwise than the signal assumed, and a stubbed runner is a copy of that assumption. What still has no directed case: a recipe that reds or cannot run, driven through the spawned binary, and the handoff-listing edge shapes — both are exercised here only as preconditions of the cases above. |
|
|
159
|
-
| [`librarian.mjs`](librarian.mjs) | The scheduled librarian's pass: reindex over both series, record age from git, the sealed-stamp re-validation nag, proposal nagging, demotion drafts, **mining** — incidents nothing in the curated layer points back at, and paths pull-request reviewers keep leaving findings on — and **consolidation**, read as budget distance plus the records citing one incident. The one tool here that reads **history** — `doctor` reads the tree and says so — which is why it belongs to a scheduled job rather than to a verify recipe: a check that reads history is a false-red generator in a shallow checkout. It renders no verdict, so it has no exit 1, and it **writes nothing itself**: the command writes, and only after the pass's own handoff is on disk, because that handoff is a member of the series one of the indexes covers. |
|
|
160
|
-
| [`control-chars.mjs`](control-chars.mjs) | **The rail on bytes a reader cannot see**, added for [#68](https://github.com/sleepy-panda-srl/portulan/issues/68): no tracked file may carry a byte in the C0 range other than TAB and LF, nor DEL. It reads a `Buffer` and **never decodes**, because decoding invalid UTF-8 yields U+FFFD — which both hides bytes and invents a character that was not there. That rule holds end to end and cost three review rounds to make true: the file contents, the file list on stdin, and a symlink's target all arrive as bytes, and a path is kept only if it round-trips through UTF-8. A binary asset is exempt **by name**, never by a content test: every binary sniff in use is keyed on NUL, so a sniff would have skipped the one file this exists to catch. The declaration is audited three ways — stale, never-read, or dead — each exit `2`. |
|
|
161
|
-
| [`control-chars.test.mjs`](control-chars.test.mjs) | Its test suite, written first. Its fixtures build their forbidden bytes with `String.fromCharCode` rather than carrying them literally — a suite that carried one would be a tracked file the check must go red on, and a rail red on its own tests is a rail somebody switches off. Its last group scans this repository's own tracked set. |
|
|
162
|
-
| [`pack-version.mjs`](pack-version.mjs) | **A change to a pack's `contributes` must move that pack's `portulan.version`** — the maintainer's ruling on [#265](https://github.com/sleepy-panda-srl/portulan/issues/265), arm 3, including that a **prose-only** edit to a fragment's `reason` counts. The **second** tool here that reads history rather than the tree, and the comparison is **three-dot** — the merge-base of the base ref and `HEAD` — so work that landed on the base is never attributed to this branch. Manifests are compared as **values**, so reindenting or reordering keys is correctly not a change. The bundle version in `packs/.claude-plugin/plugin.json` is out of scope by measurement: it is one field and the packs are many. |
|
|
163
|
-
| [`pack-identity.mjs`](pack-identity.mjs) | **Every file `npm pack` would emit is byte-identical to its staged blob (`git show :<path>`), and every one is tracked at all** — the rail [#149](https://github.com/sleepy-panda-srl/portulan/issues/149) asked for, built 2026-08-18 after the first publish measured the property by hand. It compares against the **tree**, not the registry: the registry form needs the network and a published version, so it cannot run before a first publish or offline, and a recipe that cannot run stops being run. This owns the half that can drift silently on any commit; the registry half stays a hand measurement recorded on #149 at each publish. `package.json` is compared like every other file rather than exempted, because the 2026-08-13 measurement asserted identity "`package.json` included, with no exemption" and an exemption here would quietly retire that claim. Could-not-run is **exit 2** — npm absent or unusable is not a finding about the bytes. |
|
|
164
|
-
| [`pack-identity.test.mjs`](pack-identity.test.mjs) | The rail's suite, added on a Copilot note that it had shipped without one while every comparable rail here has a suite. Exercises all four contracted states **positively** — green, a drifted byte, an untracked packed file, and could-not-run — because a failure path nobody has run is a failure path nobody has seen work. Two of its cases are the review's own findings turned into tests: a **staged** edit must compare clean, or the rail is red before every commit and contradicts [`../.portulan/dod.md`](../.portulan/dod.md); and a filename beginning with `-` must be compared rather than read as an option, which the `cat` shell-out this rail first shipped with would have done. |
|
|
165
|
-
| [`payload.mjs`](payload.mjs) | **Every `cli/*.mjs` the package would ship falls in exactly one class, and nothing arrives in the payload unclassified** — [#383](https://github.com/sleepy-panda-srl/portulan/issues/383)'s first half. `package.json`'s `files` governs the payload and nothing derived or checked its membership: `pack-identity` holds every packed file byte-identical to its source and is deliberately silent on *which* files those are. That is how `ab.mjs`, `ab-run.mjs` and `ab-grade.mjs` shipped for three sessions unnoticed, and it was demonstrated live before this was written — a `cli/` module staged into a scratch clone joined the tarball under a `pack-identity` green over 84 files. The classes are `bin` (derived from `package.json`'s own `bin`), `subcommand` (imported from `portulan.mjs`'s `SUBCOMMANDS`), `product` (unreachable but ruled to ship, with the ground for the ruling — the class `unruled` empties into as #383 is answered), `imported` (the transitive walk, which reads re-exports and side-effect imports as edges — on the grammar being total, not on a live example: an earlier draft claimed `manifest.mjs` was reachable only that way and the pre-commit checkpoint measured it false), `hook-runner` (imported from `compile.mjs`'s `HOOK_RUNNERS`, which spells them into generated host config and is their one carrier), and the **frozen** `UNRULED`. **It rosters nothing it can re-derive**, and it refuses rather than walking past a dynamic `import(` it has not accounted for. Authority for *shipped* is the `npm pack --dry-run --json` roster, never a string match on the `!` lines — npm's negation semantics are npm's. **Self-excluded**, on `eval-bundle.mjs`'s precedent: its subject is this repository's own publish surface. Could-not-run is **exit 2**. |
|
|
166
|
-
| [`payload.test.mjs`](payload.test.mjs) | Its test suite. **Every red class is forced** — an unclassified arrival, an `UNRULED` module gone reachable, one the payload stopped carrying, an exclusion the payload contradicts, a stale exclusion, an unexplained absence, and a dangling import — because a rail whose failure paths are never run is a rail nobody has seen work, and this one exists precisely because a green meant nothing for three sessions. One case asserts the frozen class is exactly thirteen; one asserts the unclassified finding *names* the refusal to widen `UNRULED`, since the cheap fix under pressure is the fail-open. The `telemetry.mjs` case deliberately asserts **two** findings rather than narrowing to one: a module that leaves the payload while staying tracked is both a stale entry and an unexplained absence, and saying so twice is the totality working. |
|
|
167
|
-
| [`version-carriers.mjs`](version-carriers.mjs) | **Every prose statement of the CURRENT version equals `package.json`'s** — the rail for a defect this repository shipped **twice with the sibling pairing already written down**: `README.md` and `.portulan/products/portulan/product.md` said `0.2.0` while everything machine-read said `0.1.0`, and one release later `README.md` said `0.1.0` inside the change cutting `0.1.1`. It matters past tidiness because `README.md` is in `package.json`'s `files` and **npm freezes a README per version**, so a wrong sentence that reaches a publish needs another release to correct — which is what `0.1.1` was. The **record layer is excluded by path**, and that is the design: this repository quotes retired sentences in the files recording their repair, so a matcher over everything reds on the account of the fix. `MUST_CARRY` closes the other half — a carrier deleted or reworded is a finding, so the rail cannot shrink in silence. What it does **not** close is a claim spelled a new way, and the accepted practice for that is #286's ruling: new spelling ⇒ a pattern here and a fixture first. |
|
|
168
|
-
| [`version-carriers.test.mjs`](version-carriers.test.mjs) | The rail's suite. Some cases run the CLI as a **subprocess whose SCRIPT PATH contains a space** — the property the entry guard turns on, and not the same thing as a spaced working directory; the rest exercise the module in process. **No count is written here on purpose.** A hand-maintained figure beside a list that grows is the defect this whole rail exists for, and `node --test` derives it. Every contracted state is exercised **positively** — green, drift in each of the three carriers, a carrier reworded away, could-not-run in three causes, and the record layer ignored. Two cases exist only because this repository's own corpus refutes the naive design: **the record must be ignored**, since `CHANGELOG.md` and two handoffs quote that retired sentence while explaining that it was retired — **despelled here on purpose**: written out in the matched spelling, this very row is a live current-version claim and the rail reds on it, which is how it was found; and **a `g` regex must not carry `lastIndex` between files**, which would find the first claim and then resume past it, reporting a clean scan over a drifted file. |
|
|
169
|
-
| [`pack-version.test.mjs`](pack-version.test.mjs) | Its test suite, written first, on **real `git init` repositories** rather than a stubbed `execFileSync` — the three-dot property cannot be observed at all without two branches and a merge-base. The shallow-clone refusal is asserted on **both** shapes a shallow clone takes, and is guarded by a full-depth clone of the same fixture reaching the red, so the `2` is the depth rather than the fixture. |
|
|
170
|
-
| [`goldens.mjs`](goldens.mjs) | **The gate-corpus runner** — milestone 8 clause (a), *golden tasks reach the gates as well as the skills*. It grades [`../evals/goldens/gates/`](../evals/goldens/gates/) against the gate policy the workspace **yields** (its own rules plus its composed packs' fragments, never the declared file alone — the class [`../.portulan/dod.md`](../.portulan/dod.md) condition 1 names), through [`compile.mjs`](compile.mjs)'s own exported `matchesRule` rather than a second implementation. **Two rails:** a rule that compiles to a matcher and carries no fixture is red, so coverage is measured rather than named; and a `documented-hole` case that starts being *caught* is red too, so a hole record cannot go stale in either direction. Resolution is **hermetic** — `discovery: null`, always — because a rail that consults the host's plugin cache grades the machine rather than the tree. **A fixture is data and is never executed**, asserted statically in the suite. What it does not establish is **adequacy**: it is a presence floor, and it prints that limit on every green. Rules with `action.none` are exempt and **named on every run**, because writing the next gate `none`-shaped is the way to dodge it. Each case records which branch of `matchesRule` it exercises, **derived** by `matcherPath` from the action kind and the tool and refused when the stored value disagrees — a field written from memory on every case in the corpus would be the second carrier this repository keeps finding wrong. An unexpected throw returns **2**, never 1: a crash that exits 1 reaches the recipe as a red about a corpus nothing finished grading. **Deliberately not a ninth `portulan` subcommand**, on `eval-bundle`'s precedent. |
|
|
171
|
-
| [`goldens.test.mjs`](goldens.test.mjs) | Its test suite. Every contracted state is exercised **positively** — a missing fixture, a regression, a hole that closed, all ten malformed-fixture refusals, and could-not-run — because a failure path nobody has run is one nobody has seen work. Three cases exist because this repository's history refutes the naive design: the entry guard must survive a path containing a space (this module shipped the broken spelling and exited **0 having run nothing**, the third time here), the denominator is asserted on the **census** rather than the verdict (both pack-contributed rules are `none`-shaped today, so a declared-only denominator would coincide — which is how that defect goes unnoticed), and the no-subprocess contract is asserted against the module's **source**, since it is structural rather than behavioural. |
|
|
172
|
-
| [`mutants.mjs`](mutants.mjs) | **The mutation census** — milestone 8 clause (b), first half, *mutation testing over both matchers*. It breaks [`compile.mjs`](compile.mjs)'s matcher region on purpose, one **anchored** operator at a time, and grades each mutant against the gate corpus. This answers the question [`goldens.mjs`](goldens.mjs) says out loud that it cannot: not *does a fixture exist* but *does the corpus NOTICE a broken matcher*. **Recorded in both directions**, as `documented-hole` is — a `killed` operator that survives means the kill-set weakened, a `survives` operator that is killed is good news the record must absorb — and a `survives` entry admits only a proof of equivalence, never an unfilled gap. **A missing or ambiguous anchor is could-not-run, never a skip**; so is a mutant that will not import, since counting an unimportable module as killed would be the loudest false green available to a tool whose whole output is a coverage claim. Every mutant gets its **own** temp directory, because ESM caches by URL with no invalidation and a reused path would grade the first mutant once per operator. **Deliberately not a ninth `portulan` subcommand**, on `eval-bundle`'s precedent. |
|
|
173
|
-
| [`mutants.test.mjs`](mutants.test.mjs) | Its test suite. The ESM-cache canary is the case worth knowing: two different mutants are imported in one process and must answer differently, which is the only assertion here that would pass on a correct runner and fail on the plausible wrong one. The refusals are exercised positively — an anchor that does not place, one that places twice, a no-op substitution, a workspace with no policy — and the entry guard is pinned under a path containing a space. |
|
|
174
|
-
| [`fuzz-shell.mjs`](fuzz-shell.mjs) | **The grammar fuzzer** — milestone 8 clause (b), second half, *grammar-aware fuzzing over the shell segmenter*, which is **two** functions here and they differ. It **composes** commands from a grammar rather than mutating strings, so it knows by construction whether the payload sits in a position bash executes or one bash only prints: the oracle is exact, and a disagreement is a gate bypass or a false red rather than something to adjudicate. **Positions are enumerated and recorded** — every cell that diverges from ground truth cites the record licensing it, in the gate map's honest-holes list or in [`compile.mjs`](compile.mjs) itself — **and spellings are fuzzed**, the invariant being that every spelling of one command in one position answers the same. Three payloads, not two, because the write matcher has two recognitions and measurement showed they diverge. **Deterministic and seeded**, seed printed on every run including the green. A finding is emitted **in the corpus's own case shape**, so it becomes a regression pin by paste. It found a live bypass of every Gated shell action — one wrapper plus one separator — on its first full run. |
|
|
175
|
-
| [`fuzz-shell.test.mjs`](fuzz-shell.test.mjs) | Its hermetic suite: determinism, budget refusals that are refused rather than coerced (`Number("")` is 0), the recorded table's totality in both directions, and the rule that a divergence must name a record. |
|
|
176
|
-
| [`fuzz-shell.ground.test.mjs`](fuzz-shell.ground.test.mjs) | **The fuzzer's oracle, measured under real bash.** A grammar that lies about itself produces not a red but a green about the wrong thing — the one failure a fuzzer cannot detect in itself — so every position is run with a **neutral** payload and every path spelling is written to a throwaway file. Separate from the file above precisely because that one handles a force-push spelling and must never spawn anything. It caught two generator defects: a `..` hop through a directory nothing created, and a marker check fooled by `echo` printing its own payload. |
|
|
177
|
-
| [`drills.mjs`](drills.mjs) | **The forced-red drill harness** — milestone 8 clause (d), *every rail forced red on a calendar and required to fire*. It asks the question its two siblings do not: [`goldens.mjs`](goldens.mjs) asks whether a gate has fixtures and [`mutants.mjs`](mutants.mjs) whether those fixtures discriminate; this asks whether the **rail still fires at all**. Every drill is a **pair** — a control on a pristine tree, then the perturbation — because a rail that only ever reds proves nothing about its green, and because a control that is already red makes the drill could-not-run rather than a fire it did not cause. Three guards, each from a defect already shipped here: a **`tell`** the rail's own output must carry when it fires and must not carry before, so a red for the wrong reason is not counted as one; an anchored substitution that must place **exactly once** *and* move bytes on disk, because both hand-run sessions had a drill that no-opped and reported on nothing; and `--only` narrowing what **runs** and never what must be well formed. Isolation is one throwaway `git worktree` per drill, so the working tree is never perturbed. **It reports on a commit and prints which one** — a dirty tree is refused outright, and `--working-copy` synthesizes one with `git stash create` and refuses while untracked-and-unstaged files exist, since a synthesized tree missing the file under review would be a green about the wrong tree. `--check` is the correspondence half the `drills` recipe runs. **Unlike its three siblings it SPAWNS by nature** — running a rail is spawning it — so what the suite pins instead is that nothing spawned is composed from a drill's declarations. The rails it does **not** drill are named on every run, with the reason: the platform floor, the host's permission layer, the CI seam, the seam scan, `claude plugin validate`, the platform watchers, and the librarian pass. **Deliberately not a ninth `portulan` subcommand**, on `eval-bundle`'s precedent. |
|
|
178
|
-
| [`drills.test.mjs`](drills.test.mjs) | Its suite. The refusals are exercised **positively** — an anchor that does not place, one that places twice, a substitution that is its own replacement, a creation over a path that exists, a drill naming an undeclared rail, a yielded rail with no drill, a drill whose control and fire are the same run — and the two host-shaped rules are pinned: a dirty tree is refused without `--working-copy`, and untracked-and-unstaged files are refused with it. The `tell` contract is asserted in **both** directions against a fake rail: present-in-fire is not enough if it was also present in the control. The entry guard is pinned under a path containing a space. |
|
|
179
|
-
| [`skill-goldens.mjs`](skill-goldens.mjs) | **The core-skill golden corpus** — milestone 8's *golden tasks per core skill*, the row's original first subject and the half clause (a) named when it said golden tasks reach the gates *"as well as the skills"*. It binds each numbered step of a skill's `## The pass` to the **live artifacts** it governs and to the rail that enforces it, or `null`. **The denominator is derived** — the steps come out of the skill file, so a step nobody wrote a case for is a red rather than a default, which is the difference between a rail and a census over a set its author drew. **An empty step list is could-not-run**: three `## The pass` spellings exist, an exact match would find one skill of three, and the other two would satisfy *every step is accounted for* vacuously. A mandate **quote must place exactly once** or the case refuses, so a reworded skill reddens instead of drifting. **The predicates are pinned by import**, not re-spelled — this module imports `RETIRE_WHEN` from [`doctor.mjs`](doctor.mjs), so the retire-when check is `doctor`'s own regex and `doctor` dropping that export breaks the runner at load. **The per-case `carrier` field is a different thing and is NOT verified**: nothing links it to an import or to any check, so a corpus naming a module that does not exist still grades green. It is declared, reviewed metadata, a reviewer is what checks it, and the runner prints that limit on every run. **`unbindable` is adjudicated**: its reason comes from a closed vocabulary, and a `judgement-only` step may name no artifacts — if you can name them it is unbuilt, not unjudgeable. The bound/unbindable ratio is printed **as a finding**, because it measures how much of a core skill is artifact discipline and how much is agent judgement. **Deliberately not a ninth `portulan` subcommand**, on `eval-bundle`'s precedent. |
|
|
180
|
-
| [`skill-goldens.test.mjs`](skill-goldens.test.mjs) | Its suite. Three cases exist because this session's own opening passes produced the defect they pin: the denominator must not go quietly empty, the EARS predicate must read the mandate's **shape** rather than a section's presence (two checkpoints computed two different figures for one row because each measured before deciding the rule), and the provenance predicate must be **shape-tolerant** because the tree carries two spellings and a literal one reports thirteen renamed files as absent. Every refusal is exercised positively — an unbindable reason outside the vocabulary, a judgement-only step naming artifacts, a step in the pass and in no case, a case naming a step the pass lacks, a corpus whose `skill` field disagrees with its filename, an undeclared slot, a slot resolving to zero files. The **both-directions** case is the one worth knowing: an accepted drift that starts complying is a finding, and it caught three filenames the author had written from a number prefix rather than read off disk. The entry guard is pinned under a path containing a space. |
|
|
181
|
-
| [`review-meter.mjs`](review-meter.mjs) | **The review-loop meter** — milestone 8 clause (c), *rounds per pull request, pushes per round, empty-round rate*. It derives the figures [`../.portulan/memory/a-review-loop-needs-a-bound.md`](../.portulan/memory/a-review-loop-needs-a-bound.md) bounds the loop on, which were counted **by hand** on 2026-07-28 and which that record says of itself **nothing checks**. Every figure is in **submission** units — the criterion predates the 2026-07-30 redefinition of *round*, and the record's own `Retire when:` names its threshold in *"the submission units of the table above, not fix-rounds"* — and the tool never prints the bare word for a figure. **Fix-rounds it does not compute at all**, and that is a finding rather than a gap: on #105 a fix rode inside another push and was never a reviewed head, and a records push after a finding-bearing submission answers nothing, so *"count pushes, then look inside each one"* is not something an API answers. The empty-round rate is reported as an **upper bound** — separating a submission that found nothing from one carrying only suppressed notes needs the awk in [`../.github/workflows/copilot-review.yml`](../.github/workflows/copilot-review.yml), a **workspace-layer** matcher reduced to one carrier, and copying it into engine code would put two spellings of one rule on opposite sides of the boundary. It **reports on a snapshot, never the network**: `--fetch` is the one mode that talks to GitHub, and it stores no review bodies. It also **detects when two of its figures are one figure** — where every submission judged its own head, pushes-per-finding-bearing-submission reduces to `1/(1 - the no-inline rate)`, which it says out loud rather than presenting as a third measurement. **A meter, not a bound**: nothing here stops a loop, and it adjudicates no sibling exemption. **Deliberately not a ninth `portulan` subcommand**, on `eval-bundle`'s precedent. |
|
|
182
|
-
| [`review-meter.test.mjs`](review-meter.test.mjs) | Its suite. Every counting trap this repository has already paid to discover is a case, because the argument for the tool is that a hand count kept getting them wrong: both reviewer logins accepted (a filter on one returns zero from the other, #154), our own reviews refused as **could-not-judge** rather than counted (at merge: #105 is seven of fifteen, #342 is ninety of a hundred and two), a duplicated pull request caught, and an absent inline count refused because absent is not zero — as is an absent `head`, an unparsable `mergedAt`, and a window heading disagreeing with the corpus under it. The empty denominator returns **null and never zero** — an unmeasured window is not a window below the retirement threshold — and the boundary is asserted **at** the threshold, since *below* read generously retires a rule the measurement did not. The register's byte-compare is exercised in both directions, and the entry guard is pinned under a path containing a space: this module shipped the broken spelling and `--fetch` exited **0 having written nothing**, the fourth time here. **Three cases assert an error MESSAGE rather than only an exit code**, each because the case they replaced passed for the wrong reason — a missing-register case that never reached the register, a `--pool` case that fell through into the real fetch and got its 2 from `gh` failing, and a `--check` that compared nothing and returned 0. An assertion holding for a reason other than its subject keeps passing after the code it names is deleted. |
|
|
183
|
-
| [`telemetry.mjs`](telemetry.mjs) | **The OTel emitter** — milestone 8's *OTel opt-in config*, and the first thing here that can send anything anywhere on purpose. The reading was **ruled, not derived** (2026-08-28): a validated opt-in config surface **plus a real emitter**, hand-written OTLP-over-HTTP JSON with no OpenTelemetry SDK, because an `@opentelemetry/*` import would end the zero-dependency property [`../.portulan/identity.md`](../.portulan/identity.md) protects — the maintainer's 2026-07-31 ruling that this ships as plain ESM, which [`../.portulan/repos/portulan.md`](../.portulan/repos/portulan.md) records as permanent rather than pending. **The committed config is the only gate**, and that is a deliberate departure from the OTel environment contract: `OTEL_SDK_DISABLED` defaults to *false*, so honouring the standard env surface as the gate would make an adopter who already exports an endpoint for some other service start emitting without deciding to — an opt-**out**, where the criterion says opt-in. The environment supplies **transport only** (`OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_HEADERS`), so a token never enters a committed file. `--export` is the one mode that reaches the network and it refuses twice over: when the config opts out, and in the **three** states that are not a committed consent: the config is **untracked**, **tracked but absent from `HEAD`** (staged and never committed), or **differs from `HEAD`**. The staged case is the one worth naming — it is how an agent would manufacture consent one step short of a commit, and a round of this pull request's own review had made it unreachable. **Opted-out and could-not-read are different answers with different codes**: a config with a typo its author meant as `true` must never report *opted out*. Figures are **Gauge** points stamped with the instant the measurement is *about* — the snapshot's own `captured` — not a clock read, which is both what OTLP means and what makes the golden byte-stable. It also carries `--audit-recipes`, the rail behind the standing prohibition on a network call inside a verify recipe: it sweeps the set the workspace **yields** at a pinned root, rails the **class** of network modes rather than this session's own, and refuses an empty set rather than passing vacuously. **Deliberately not a ninth `portulan` subcommand**, on `eval-bundle`'s precedent. |
|
|
184
|
-
| [`telemetry.test.mjs`](telemetry.test.mjs) | Its suite. **No case opens a real socket** — [`../.portulan/verify/tests.sh`](../.portulan/verify/tests.sh) runs this suite, so a test that did would be the network call inside a verify recipe that this module's own audit forbids; the transport is injected and the send is proven against a temporary git repository with the consent actually committed, asserting the wire bytes **are** the golden's bytes. Every refusal is exercised positively: an absent `enabled` (absent is not false), a secret in a committed config, an unknown signal, a wrong spec, an unparsable capture stamp, an edited consent, a consent outside the tree, two modes at once — and a valid config producing **no** problems, without which a validator that refused everything would satisfy the rest. The closed attribute vocabulary is pinned **in both directions**, so an added key and a deleted carrier are each a red, and separate cases assert that no reviewer login and no commit SHA reaches the payload. The producer seam is exercised against a producer this module **did not ship**, because a generic shape with one implementation is a capability claim nothing checks. The entry guard is pinned under a path containing a space — the fourth instance here, and the one where silence is indistinguishable from correct behaviour. _(Three cases failed on first run because `withTemp` was synchronous around async bodies and deleted the directory mid-test — a helper that tidies up before the test runs is a false red today and a false green one assertion later. A fourth case **passed** and should not have: the non-2xx collector arm pointed `--repo-root` at a directory with no snapshot, so it exited 2 at the signal read and never called the injected transport, leaving that branch covered by nothing. Caught at the pre-commit checkpoint; it now asserts the send happened, and a mutant deleting the branch kills it.)_ |
|
|
185
|
-
| [`eval-bundle.mjs`](eval-bundle.mjs) | **The evaluation-bundle cutter** — a named-recipient stamped copy of the shippable payload, cut from an explicitly named commit, ported into the tree on the maintainer's ruling of 2026-08-17. The payload partition and the machine-read license census are **pinned in both directions**, and **two different steps refuse two different things**, which is worth keeping straight because each sends an operator somewhere else: the **census** runs on the freshly materialised cut and refuses an Apache assertion in a file that is not rostered in `APACHE_MANIFESTS`, or a rostered file that has stopped asserting; the **guard** runs on the finished cut and — **inverted on the maintainer's ruling of 2026-08-18 ([#284](https://github.com/sleepy-panda-srl/portulan/issues/284))** — refuses any machine-read `license` value that is **not** Apache-2.0, plus any issuer-machinery path that leaked in, diagnosing a drifted rostered manifest separately from an unrostered one and from a failed self-exclusion. Nothing rewrites licence metadata and `LICENSE` ships: the bundle is the public tree's bytes under the public tree's licence, so the README's own License section is correct as written and its link resolves. The issuance ledger, and all recipient data, stay OUTSIDE this repository; evaluation terms render from [`eval-license.template.md`](eval-license.template.md) **at the payload commit**, pinning payload and terms to one sha; `--check` (what [`../.portulan/verify/eval-bundle.sh`](../.portulan/verify/eval-bundle.sh) runs) cuts the **index**, wrapped as an unreferenced probe commit, for a fixture recipient into scratch it always deletes. **Deliberately not a ninth `portulan` subcommand**, on `discover`'s precedent above — and deliberately **self-excluded from the payload**, so an evaluee never receives the issuer's machinery. |
|
|
186
|
-
| [`eval-bundle.test.mjs`](eval-bundle.test.mjs) | Its test suite. The rosters are pinned with the suite's **own** instruments — `git ls-tree` and `git grep`, never the module's helpers — because a pin that asks the subject to measure itself tests agreement, not truth; the self-exclusion is exercised **positively** on a fixture repository that plants files at exactly the excluded paths; the terms pin is **demonstrated** (an old commit stamps its old template whatever the tree says now); and the plumbing transport is asserted byte-identical, modes included, to `git archive \| tar -x`, so the port's one mechanical delta is demonstrated rather than trusted. |
|
|
187
|
-
| [`eval-license.template.md`](eval-license.template.md) | **The evaluation-license template** — four placeholders (`{{name}}`, `{{login}}`, `{{date}}`, `{{shortSha}}`), rendered into a bundle's `EVAL-LICENSE.md` by [`eval-bundle.mjs`](eval-bundle.mjs), which reads it **from the payload commit with `git show`, never from the working tree** (the supervisor's ruling of 2026-08-17: payload and terms pin to one sha in the stamp's `source_commit`, so a later edit here cannot drift under an already-stamped bundle). Issuer machinery, so **self-excluded from the payload**: an evaluee receives the stamped license, never the stamp press. The repository is public, so this text is world-readable, and the maintainer settled its wording on 2026-08-17: the bundle's files carry Apache-2.0, the same terms as the public repository, so the template's "non-public materials" and no-redistribution clauses are withdrawn. The mechanism is indifferent — it stamps whatever the payload commit carries. |
|
|
188
|
-
| [`librarian.test.mjs`](librarian.test.mjs) | Its test suite, written first, on real scratch git repositories rather than an injected clock — a fake history proves nothing about the one call this tool exists to make. |
|
|
189
|
-
| [`ab.mjs`](ab.mjs) | **The A/B arm builder** — milestone 8's *A/B baseline recorded* clause, session 6b. It builds the two arms [`../evals/ab/arm.md`](../evals/ab/arm.md) specifies and **grades neither**: no agent is run here and no scenario is graded — the graders and their three classes of discrimination fixture are [`ab-grade.mjs`](ab-grade.mjs)'s, the run [`ab-run.mjs`](ab-run.mjs)'s, on the maintainer's ruling of 2026-08-29. Construction is **stage-then-vendor**, and that order is forced rather than chosen: [`vendor.mjs`](vendor.mjs) refuses this workspace outright while `constitution` resolves outside `.portulan`, so dropping that slot is a precondition and not only a faithfulness move. **Its disposition table is TOTAL and it refuses to build when it is not** — the defect it exists to make impossible is measured, not imagined: built to `arm.md`'s six rows and vendored, the treatment arm still carried customer zero's `memory-index.md` (30 rule titles over an empty store), `handoffs-index.md` (146), `rule-carriers.json` (five entries naming the A/B clause) and `labels.json`, with `doctor` GREEN over all of it, because `vendor` carries every ordinary file under a workspace directory and a specification that names what to REMOVE leaks whatever it forgot. It also gives `arm.md`'s **rule 2** its first checker — *no move authors a normative sentence* — and that checker is a **keyword matcher, not a reading of English**: the `deletion` and `emptying` kinds are covered in full because they add no sentence, while `substitution` is covered by a declared marker vocabulary that **misses a paraphrase carrying the mandate and none of the words**. One case in its own corpus is exactly that miss and the suite requires it to stand. `--stop-probe` is [`../evals/ab/corpus.md`](../evals/ab/corpus.md)'s acceptance test and is **not** a recipe mode: it runs one real agent turn, because a fixture calling `stop-gate.mjs` would prove the script records and assume the thing under test — whether the host invokes the hook at all. **Deliberately not a ninth `portulan` subcommand**, on `eval-bundle`'s precedent. |
|
|
190
|
-
| [`ab.test.mjs`](ab.test.mjs) | Its suite. **No case runs an agent** — [`../.portulan/verify/tests.sh`](../.portulan/verify/tests.sh) runs it, and a test spawning `claude` would put an agent turn inside a verify recipe; the probe's refusals are exercised against `false` standing in for an agent that cannot start. The distinctions that cost something are each a case: an unclassified path is exit **1** and a stale disposition exit **2**, because a finding about the tree is not a could-not-run and the first cut collapsed them; a **declared** substitution is un-substituted before comparison while an undeclared one reads as an authored sentence, which is the property that makes the matcher auditable; and `mayBeAbsent` is audited on its **reason** — git carries nothing under the path — rather than on the path's presence, because `personas/` is present in a working copy and absent in a checkout and an audit keyed on presence refused every working-copy run. That last one was found by this rail's own drill on its **control** leg, which is the argument for having a control leg. The corpus case requiring a **documented miss** is not a tautology over the table: a corpus in which everything is caught proves only that the author picked cases the matcher already handled. |
|
|
191
|
-
| [`ab-grade.mjs`](ab-grade.mjs) | **The A/B graders** — milestone 8's *A/B baseline recorded* clause, session 6c, the middle of the maintainer's construction \| grading \| running split. It carries the four scenarios' **stimuli**, the four graders, and the three classes of discrimination [`../evals/ab/corpus.md`](../evals/ab/corpus.md) requires. **It runs no agent and records no baseline figure**: every figure it produces is about the instrument, and the run is 6d's. The stimulus ships here because a level-1 fixture *is* a staged tree plus a mutation — `stageScenario()` writes files and **spawns nothing**, and this module imports no child-process API at all. Each grader recomputes its attribution anchor from the scenario definition and **refuses** a tree it cannot attribute, which is neither a pass nor a fail; the nonce is never read out of arm output, since *"a grader that trusted arm output for its nonce could be satisfied by an arm naming its own"*. **The blindness it exists for is measured**: a prototype grader returned *"30 records, each with its own provenance and retirement condition"* against a vendored arm that had done nothing at all — it passed level 1, and only **inversion at the delta** caught it. `done-demonstrated` has no `demonstrated` verdict, deliberately: the rig fails unconditionally, so a green rail is reachable only by rewriting the check, and grading that as compliance would score the gaming move as the good outcome. |
|
|
192
|
-
| [`ab-grade.test.mjs`](ab-grade.test.mjs) | Its suite. **No case runs an agent**, for the reason `ab.test.mjs` gives. The cases that cost something: a staged-and-untouched tree grades to its **declared** inert verdict, which is the prototype's failure asked directly rather than approximated; two of the four scenarios are compliant when inert and the suite pins **which**, because not destroying a target and not claiming completion are both satisfied by doing nothing; a constant grader is **reproduced** and shown passing level 1 while failing level 2; and the positive control [`../evals/ab/corpus.md`](../evals/ab/corpus.md) named as unbuilt is built at its real size — a stub agent proves the stop probe's **read path**, and proves nothing about the host invoking the hook, which stays a by-hand run. |
|
|
193
|
-
| [`ab-run.mjs`](ab-run.mjs) | **The A/B run** — milestone 8's *A/B baseline recorded* clause, session 6d, the last third of the maintainer's construction \| grading \| running split, and **the only module in this family that spawns an agent**. It is separate from [`ab-grade.mjs`](ab-grade.mjs) because that file says of itself, in three carriers and one rail, that it runs no agent and imports no child-process API at all. **There is no `--operator-env` flag**: every turn runs isolated with a fresh home and config directory of its own — forty, not two — because [`../evals/ab/corpus.md`](../evals/ab/corpus.md) forbids a baseline over an unisolated arm, and a departure is a visible code change plus a maintainer's ruling rather than a flag. `did-not-complete`, `could-not-attribute` and `non-compliant` are three different facts and the four states are total over `k`. **The invocation was ruled by measurement**: `--print` alone left both arms byte-unchanged after completed turns, so `acceptEdits` joins it — which auto-approves edits the settings do not forbid and does **not** override a `deny` rule, so arm A's compiled enforcement survives. The record is a committed snapshot plus a register rendered from it and byte-compared, on [`review-meter.mjs`](review-meter.mjs)'s precedent: the events do not repeat, so the unreproducible half is the snapshot alone and it says so on its own face. |
|
|
194
|
-
| [`ab-run.test.mjs`](ab-run.test.mjs) | Its suite. **No case runs a real agent** — `runTurn` takes its agent as a parameter and a stub script stands in, exactly as `ab.test.mjs` does for the stop probe, because a test spawning `claude` would put an agent turn and a credential inside a verify recipe. The cases that cost something: passing `--operator-env` is a refusal rather than a silent ignore; a flag that would dissolve arm A's enforcement is refused at the turn and again in the record; the operator seed is asserted to touch onboarding and trust and **nothing else**, since a seed reaching permissions would silently become treatment; stdin is closed, so a prompt the harness cannot answer fails fast instead of waiting out a ten-minute timeout, which one real turn already did; and a published cell that has drifted from its own per-turn rows is red. |
|
|
195
|
-
| [`release-eval.mjs`](release-eval.mjs) | **The eval result a release carries** — milestone 8's ninth clause, added by the maintainer's ruling of 2026-08-24, which made a Protocol obligation older than the row bind that row's close. A record is a pair per version under [`../evals/releases/`](../evals/releases/): a capture holding the verdict every recipe the workspace **yields** returned at a named commit, and a register rendered from it and byte-compared. The A/B baseline is cited **by identity and never by figure** — path, capture date, commit, cleanliness — because its cells have one carrier and a second copy inside a release record is the hand-maintained figure this milestone deleted six of. **Two designs were reversed before a line was written.** Cut detection off `CHANGELOG.md`'s top heading never fires: the cut re-seeds `## Unreleased` above the version it just wrote, so the top heading is `Unreleased` on the cut commit too. And a rail keyed to `package.json`'s version alone grades one record at a time — once `0.1.4` is declared, `0.1.3`'s record can be deleted in silence — so the subject is every version the changelog records from `0.1.3` onward, permanently, graded in both directions. Its **own** row is excluded from every capture, with the reason printed, because a capture cannot be accurate about the record it is inside. `--tagged` is the release act's mode and the only check here that sees a release rather than a tree; [`../.github/workflows/publish-github-packages.yml`](../.github/workflows/publish-github-packages.yml) runs it before publishing. **Deliberately not a ninth `portulan` subcommand**, on `eval-bundle`'s and `ab`'s precedent. |
|
|
196
|
-
| [`release-eval.test.mjs`](release-eval.test.mjs) | Its suite. **No case runs the recipe set** — `--capture` spawns every rail in the workspace, and a test that did would put the whole suite inside one of its own members; every case exercises the record layer instead, which is what the rail reads. The cases that cost something: the changelog fixture is shaped like the real file, with a re-seeded `## Unreleased` on top and an entry that *quotes* a heading, so a looser matcher fails it; an older governed record deleted after a newer release is cut is red, which is the case the replaced design passed; a missing **boolean** is caught by name, because it renders as a branch and its absence publishes a claim the capture never made rather than leaving a hole a render probe could see; a record for a release the changelog never cut is red; and `--write` refuses a capture it could not read rather than rendering from one. |
|
|
197
|
-
| [`checkout-refs.live.test.mjs`](checkout-refs.live.test.mjs) | **A workflow that checks out more than once names the ref of every checkout after the first.** The rail for a defect this repository shipped **twice at one step**: [`publish-github-packages.yml`](../.github/workflows/publish-github-packages.yml) grades the tagged tree with a grader that must not come *from* it, since `release-eval.mjs` exists in no tag before `v0.1.3` — and the second checkout added to fix that carried no `ref`, which `actions/checkout` defaults to *the event's own* ref, the tag on `release: published`. The crash came back inside its own repair. **The rule is narrow on purpose and the narrowness was measured:** five of the seven checkouts in `.github/workflows/` carry no `ref` and every one is correct, so a blanket rule would red five right answers to expose one wrong one — which is how a recipe gets switched off. Checking out *twice* is a statement that two different trees are wanted, so the second cannot inherit a default whose value depends on which event fired. It does **not** establish that the ref named is the right one — an explicit tag on a grader would pass and be the same bug — only that the choice was made rather than inherited. |
|
|
198
|
-
| [`recipe-set.mjs`](recipe-set.mjs) | **The recipe set**, the one carrier of *what this workspace runs*. Row 7's amendment of 2026-07-31 ruled that a composed pack's verify recipes reach the adopting workspace's runnable set, under a contract stated in the row rather than left to this file: **additive only** — a pack may add a recipe and may never redefine, remove or replace one the workspace declares, nor become `verify.default` — and **namespaced by its pack, so a collision is impossible rather than resolved**. |
|
|
199
|
-
| [`recipe-set.test.mjs`](recipe-set.test.mjs) | Its test suite, against a fixture resolver: the additive-only rule, the namespacing, the refusal to become `verify.default`, and **could-not-run — exit 2, never silently absent** when a composed recipe cannot resolve. |
|
|
200
|
-
| [`recipe-set.live.test.mjs`](recipe-set.live.test.mjs) | The composed set against **this repository** rather than against fixtures, and it exists because the unit suite is not enough — measured, not supposed, on the change that added both. `compile.mjs`'s `resolvePack` returns `manifest` as the **path** to `pack.json`, not the parsed object; the first cut of `resolverFor` treated it as parsed, so every composed pack contributed nothing while the unit suite stayed green, because its fixture resolver returned a parsed object. A harness that supplies the shape the code assumes cannot find the assumption. |
|
|
201
|
-
| [`rule-carriers.mjs`](rule-carriers.mjs) | **The rail that keeps a reduced rule reduced** — [`0027`](../.portulan/proposals/0027-a-reduced-rule-stays-reduced.md). A rule an incident has reduced to ONE carrier is registered with the spellings its other carriers used; those spellings may then appear only in the carrier, or beside a citation of it. |
|
|
202
|
-
| [`rule-carriers.test.mjs`](rule-carriers.test.mjs) | Its test suite, and **the fixtures are the instrument's control cases** in [`0028`](../.portulan/proposals/0028-a-records-world-claim-carries-its-instrument.md)'s sense: a counter with no control is a claim rather than a measurement, and a fixture test is the control made permanent. The two that matter most are `normalise`'s — a markdown link inside the phrase, and a wrapped line — both of which this instrument got wrong on its first real run. |
|
|
203
|
-
| [`pinned-roots.live.test.mjs`](pinned-roots.live.test.mjs) | **The required checks name their resolution root, and this is what makes that a rail.** A pre-commit checkpoint stripped all six `--pack-root packs` from the tree at once and everything stayed green: eleven recipes, 1535 tests, nothing red. The pins were shell prose. Removing the one in `doctor.sh` silently reverts `examples/` to green-by-not-looking, which is the exact fail-open the pins were added to close — **a containment nothing enforces is a comment**. |
|
|
204
|
-
| [`list-quoting.live.test.mjs`](list-quoting.live.test.mjs) | **Every recipe that enumerates the tree reads the pathname git carries, not a quoted spelling of it** — the rail from [#209](https://github.com/sleepy-panda-srl/portulan/issues/209), which reported the wrong mechanism. Without `-z`, git C-quotes a control character *regardless of `core.quotePath`*, so a newline never mis-splits a list; it arrives as one line, `"we\nird.md"`. What the line-based form loses is the **spelling**, and that reaches `café.mjs` and not only exotic names. Both directions were live: three recipes turned a quoted path into a **false red**, and `docs.sh`'s cli-table enumeration silently dropped the file and passed — **the only false green of the class**. It accepts `-z` *or* `core.quotePath=false` because the two fail differently, and `docs.sh` cannot take `-z` without arming the `\001` sentinel collision its links `awk` depends on. |
|
|
205
|
-
| [`test-isolation.live.test.mjs`](test-isolation.live.test.mjs) | **A test that substitutes a shared object hands the restore to the runner** — the rail from [#254](https://github.com/sleepy-panda-srl/portulan/issues/254), whose own instrument swept for the NAME `fs.*Sync =` and so could not see the same shape wearing another. Sweeping by SHAPE found fourteen sites, not six: `process.stdout.write` and `process.stderr.write` too. Eleven converted to `t.mock.method`; **three deliberately not**, because the runner scopes a mock to the TEST and those mean a helper call, a loop iteration, or a `process.env` restore — and `t.mock.property` *cannot* take that last one, throwing `ERR_INVALID_OBJECT_DEFINE_PROPERTY` on `process.env` because it installs an accessor pair. It binds the uniform half using the issue's own retire-when as its matcher, so the rule and the rail cannot drift. |
|
|
206
|
-
| [`cli-roster.live.test.mjs`](cli-roster.live.test.mjs) | **The roster beside the eight is a partition of this directory, and no part of it is counted by hand** — the rail from [#204](https://github.com/sleepy-panda-srl/portulan/issues/204), whose reporter had read three carriers that disagreed and concluded a tool had been removed. Two of the three now cite the third; what stayed unrailed was the surviving sentence, and it drifted again — `version-carriers.mjs` and `inside.mjs` reached the table and neither roster sentence. The eight come from a real import of `SUBCOMMANDS`, never a copied list; everything else must be named between the roster markers. Both directions, because both have failed. The **numerals are gone rather than corrected**, on this file's own convention about hand-maintained figures — a count nobody writes cannot go stale. |
|
|
207
|
-
| [`shadowed-pack.test.mjs`](shadowed-pack.test.mjs) | **Two roots answered for one declared pack, and the compiler refuses rather than picking** — the rail from [#316](https://github.com/sleepy-panda-srl/portulan/issues/316). Discovered-first resolution meant a bare `compile` emitted the installed copy's policy while `verify/compile.sh` read the tree's; on this project's own host that re-introduced a `git commit --no-verify` matcher the tree had deliberately removed as false coverage. Hermetic by **injecting the discovery thunk** rather than faking a plugin cache, since a fake host would re-test `discover.mjs`'s record reader and tie these cases to a schema they say nothing about. The edge that keeps the refusal honest is **elected** discovery: refusing `--pack-root auto` would refuse a caller for answering the very question the refusal asks. |
|
|
208
|
-
| [`shadowed-resolution.test.mjs`](shadowed-resolution.test.mjs) | **The same refusal in the two tools `compile`'s could not reach** — `index` and `recipe-set` resolve packs without going through `packContributions`, so [#316](https://github.com/sleepy-panda-srl/portulan/issues/316)'s guard was structurally out of range. Its first two cases are the demonstration [#318](https://github.com/sleepy-panda-srl/portulan/issues/318) was filed owing: the two worlds digesting a different memory scope, and composing run lines that point at different files. Both are shown through the **elected** spellings, because the bare path now refuses — a fixture claiming to show divergence there would be showing the refusal instead. |
|
|
209
|
-
| [`gh-bot.test.mjs`](gh-bot.test.mjs) | The agent identity's wrapper refuses the endpoints it is not for. Its subject, [`../.portulan/tools/gh-bot`](../.portulan/tools/gh-bot), is customer-zero operator tooling and not part of the product — but the suite lives here anyway, because [`../.portulan/verify/tests.sh`](../.portulan/verify/tests.sh) runs exactly `cli/**/*.test.mjs` and that recipe's stated design is that **a new suite joins by existing rather than by being named**. |
|
|
210
|
-
| [`fixtures/`](fixtures/) | Known-bad manifests, and a workspace whose repo card has drifted from its tree. |
|
|
211
|
-
|
|
212
|
-
```
|
|
213
|
-
portulan <subcommand> [options] # or: node cli/portulan.mjs <subcommand> [options]
|
|
214
|
-
|
|
215
|
-
node cli/init.mjs --residence <in-repo|pointer> [options] <repository-dir>
|
|
216
|
-
node cli/new.mjs <kind> <name> [--into <dir>] [options]
|
|
217
|
-
node cli/vendor.mjs <workspace-dir> --into <dir> --residence <in-repo|feed-side> (--host <id> | --switch)
|
|
218
|
-
node cli/doctor.mjs <workspace-dir> [<workspace-dir> ...]
|
|
219
|
-
node cli/plugin-lint.mjs [--payload] <plugin-root> [<plugin-root> ...]
|
|
220
|
-
node cli/compile.mjs [--workspace <repository-dir | workspace-dir>] [--check]
|
|
221
|
-
node cli/index.mjs [--check] <workspace-dir> [<workspace-dir> ...]
|
|
222
|
-
node cli/upgrade.mjs [--check | --write] [--tree <path>] <workspace-dir>
|
|
223
|
-
node cli/librarian.mjs [--as-of YYYY-MM-DD] [--write] [--log <path>] [--reviews <path>] <workspace-dir> [...]
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
The two validators: exit `0` when every workspace or plugin root validates · `1` when at least one does
|
|
227
|
-
not · `2` could not run. `compile`: exit `0` when it wrote, or agreed under `--check` · `1` only under
|
|
228
|
-
`--check`, when the artifact is missing or has drifted · `2` could not run — writing never returns `1`,
|
|
229
|
-
because a run that rewrites the artifact has nothing to disagree with. `init` and `new` have only **two** codes —
|
|
230
|
-
`0` it wrote · `2` it wrote nothing — and the missing `1` is the point: they render no verdict about
|
|
231
|
-
anybody's workspace, so they have no red to report. _(`2` covers every way of writing nothing, a human declining at the confirmation included — `0`
|
|
232
|
-
must keep meaning **it wrote** for anything chaining on it.)_ `vendor` **does** have a `1`, and the asymmetry is
|
|
233
|
-
worth a sentence: it runs the real validator at both ends of a switch and reports its verdict, so
|
|
234
|
-
collapsing *the workspace I materialised is invalid* into *I could not run* would leave a caller unable
|
|
235
|
-
to tell a bad workspace from a missing flag. `index` uses the same three
|
|
236
|
-
codes and differs from `compile` in one way worth knowing before reading it: **writing can still return
|
|
237
|
-
`1`**, because a budget breach is a verdict about the store rather than about the artifact, and the
|
|
238
|
-
artifact is written anyway so there is something to consolidate from. `upgrade` uses all three and its
|
|
239
|
-
`1` is the widest here — steps owed under `--check`, a workspace `doctor` reds **before** the migration,
|
|
240
|
-
a migrated workspace that comes back red and is rolled back, **and a pointer that does not resolve**
|
|
241
|
-
(`not-installed` or `ambiguous`, which is [`discover.mjs`](discover.mjs)'s own mapping carried through
|
|
242
|
-
rather than a second opinion about it). Its `2` covers the two directions it cannot help with — a
|
|
243
|
-
workspace ahead of this bundle, and one behind by a MAJOR that no step reaches — plus a step that
|
|
244
|
-
**could not tell**, and a `--write` aimed at an installed workspace. A bare run with no flag prints the
|
|
245
|
-
plan and exits **0**, on `skills-set`'s precedent, because reporting is not a verdict. Both workspaces this repository
|
|
246
|
-
owns are validated on every pull request, because
|
|
247
|
-
[`../.portulan/workspace.json`](../.portulan/workspace.json) declares
|
|
248
|
-
[`../.portulan/verify/doctor.sh`](../.portulan/verify/doctor.sh) as a verify recipe and CI runs every
|
|
249
|
-
recipe the manifest **yields** — the workspace's own, plus those the packs it composes contribute.
|
|
250
|
-
|
|
251
|
-
## What `doctor` checks
|
|
23
|
+
| [`portulan.mjs`](portulan.mjs) | Portulan — the one entry point the `npx` package exposes. |
|
|
24
|
+
| [`init.mjs`](init.mjs) | `portulan init`: draft a workspace for a repository that has none. |
|
|
25
|
+
| [`doctor.mjs`](doctor.mjs) | `portulan doctor`: validate a workspace and report branch-owned host tokens beside local receipts; --static skips usage records. |
|
|
26
|
+
| [`compile.mjs`](compile.mjs) | `portulan compile`: compile a workspace's gate policy into host enforcement. |
|
|
27
|
+
| [`vendor.mjs`](vendor.mjs) | `portulan vendor`: materialise a workspace where it is needed: into a host, or between residences. |
|
|
28
|
+
| [`index.mjs`](index.mjs) | `portulan index`: regenerate the memory, handoff and scope indexes. |
|
|
29
|
+
| [`upgrade.mjs`](upgrade.mjs) | `portulan upgrade`: migrate a workspace: the Workspace Definition steps it owes, and the repairs a rewriter owes it. |
|
|
30
|
+
| [`new.mjs`](new.mjs) | `portulan new`: scaffold a skill, persona, pack, workspace, gate policy or repo card from a core template. |
|
|
31
|
+
| [`feedback.mjs`](feedback.mjs) | `portulan feedback`: file an issue from a report you previewed, seam-scanned before it leaves the machine. |
|
|
252
32
|
|
|
253
|
-
|
|
254
|
-
naming the violated constraint and its location. The validator implements exactly the JSON Schema
|
|
255
|
-
subset [`../spec/README.md`](../spec/README.md) names, and **refuses a schema that reaches outside it**
|
|
256
|
-
rather than ignoring the keyword — a validator that skips what it does not know reports conformance it
|
|
257
|
-
never checked.
|
|
258
|
-
- **Path slots resolve**, as files or directories per the schema. A slot resolving outside the workspace
|
|
259
|
-
is reported, never failed; `constitution` and `tree` are expected to.
|
|
260
|
-
- **Cross-references hold** — `verify.default` names a real recipe, `products[].repos` names real cards.
|
|
261
|
-
- **Claims against the tree**, where the workspace declares one: a repo card's build/test/run and layout
|
|
262
|
-
paths, and the status check the gate map says `main` requires.
|
|
263
|
-
- **Rule provenance** — every `type: rule` memory record carries a well-formed link or sealed stamp, and
|
|
264
|
-
the **sealed proportion** is reported, because a workspace where every rule is sealed has opted out of
|
|
265
|
-
retirement altogether.
|
|
266
|
-
- **The store's growth is reported, never judged** — record count and total size on every run, plus a
|
|
267
|
-
note naming any record that states no `Retire when:` condition, because a record no condition can
|
|
268
|
-
demote is how a store only grows. Notes, not failures: no rule mandates the field. Ages are absent on
|
|
269
|
-
purpose — doctor reads the tree, never git, and in a fresh clone every mtime is checkout time, so an
|
|
270
|
-
age report from here would be fabrication; staleness is the librarian's (milestone 5).
|
|
33
|
+
### Hook runners
|
|
271
34
|
|
|
272
|
-
|
|
35
|
+
Run by the hooks `compile` writes into a host's settings, never by a person.
|
|
273
36
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
for reasons unrelated to the change under test.
|
|
280
|
-
- **It never judges truth.** A fabricated sealed stamp passes exactly as a real one does; a path that
|
|
281
|
-
resolves says nothing about whether the document at the end of it is still accurate.
|
|
282
|
-
- **It scores agent legibility, and the score moves no exit code.** **Seven** dimensions, every one of them
|
|
283
|
-
optional in the Workspace Definition so it can genuinely be absent, printed as one line naming what
|
|
284
|
-
was missed. *Whether the workspace declares verification at all* was an eighth until a checkpoint
|
|
285
|
-
measured it: the schema requires `verify` of every workspace the score can reach, so it was a
|
|
286
|
-
constant dressed as a measurement. The `affordances` slot is the named input and not the only one — six manifest keys are
|
|
287
|
-
read beside it, because a score confined to the slot could not tell two workspaces apart. What it
|
|
288
|
-
cannot do is judge the prose: the limits dimension is a **form** check over a named heading table, so
|
|
289
|
-
a section with an empty body passes it.
|
|
290
|
-
- **It cannot see live settings.** The gate-map lint compares a claimed status-check name against job ids
|
|
291
|
-
in the tree. Whether branch protection actually requires that check, and which app it is pinned to, are
|
|
292
|
-
API facts `doctor` does not fetch.
|
|
293
|
-
- **The per-host report covers two backends, and two is not many.** `compile --matrix` and `doctor`'s
|
|
294
|
-
`enforcement` notes account for every rule against the Claude Code host and the GitHub repository
|
|
295
|
-
ruleset. A host with neither — a different agent runtime, a different forge — has no column here, and
|
|
296
|
-
the matrix says nothing about it rather than implying coverage.
|
|
297
|
-
- **Exported-versus-live drift is not checked by anything automatic.** `compile` proves the exported
|
|
298
|
-
ruleset matches the policy. Whether the repository's *live* settings match the export is a fact about
|
|
299
|
-
an API `doctor` does not call and a recipe may not reach; it is compared at the supervised checkpoints,
|
|
300
|
-
by hand, and the milestone-4 row says so.
|
|
301
|
-
- **`compile` cannot tell whether the host honours what it emits.** It proves the artifact matches the
|
|
302
|
-
policy. That a permission rule or a hook is actually enforced is a fact about a running host, measured
|
|
303
|
-
at the supervised checkpoints — see
|
|
304
|
-
[`../.portulan/memory/a-manifest-field-can-validate-and-load-nothing.md`](../.portulan/memory/a-manifest-field-can-validate-and-load-nothing.md)
|
|
305
|
-
for what it costs to assume otherwise.
|
|
306
|
-
|
|
307
|
-
## What `plugin-lint` checks
|
|
308
|
-
|
|
309
|
-
Both manifests parse and are objects; `plugin.json` has a kebab-case `name` and, if it declares one, a
|
|
310
|
-
SemVer `version`; `marketplace.json` has a name, an owner, and **at least one plugin**; every entry has a
|
|
311
|
-
name and a source; every relative source and every component path starts with `./`, stays inside the
|
|
312
|
-
tree, and resolves; the marketplace entry that points at the plugin root agrees with `plugin.json` about
|
|
313
|
-
name and version; every declared skill — and every agent at `./agents/`, which the manifest cannot
|
|
314
|
-
declare without suppressing it — is a real file with frontmatter, a kebab-case
|
|
315
|
-
`name`, and a non-empty `description`. A skill found in the tree but covered by no declared path is
|
|
316
|
-
**reported** — declared is what ships, and an undeclared skill is one its author believes is shipping.
|
|
317
|
-
|
|
318
|
-
Two of those are **stricter than the platform**, on purpose, and are this repository's invariants rather
|
|
319
|
-
than claims about the contract. `name` is optional to Claude Code: omit it and the invocation name is
|
|
320
|
-
inherited from the layout — the directory name for a skill in a `skills/<dir>/` subdirectory, but the
|
|
321
|
-
*install* directory for a path pointing straight at one, which on a marketplace install is a version
|
|
322
|
-
string that changes with every update. Requiring it makes a skill's name a property of the skill.
|
|
323
|
-
And a component path is required to resolve **after canonicalisation**, not merely lexically, so a
|
|
324
|
-
symlink out of the tree is caught rather than read as containment.
|
|
325
|
-
|
|
326
|
-
**What it is not.** It is not an implementation of the Claude Code plugin contract, and describing it as
|
|
327
|
-
one would be the overclaim this repository forbids. `claude plugin validate --strict` is that authority
|
|
328
|
-
and runs at the supervised checkpoints rather than in CI, because CI here installs nothing and a recipe
|
|
329
|
-
declaring the `claude` binary would exit `2` on every run. The two are not nested in either direction:
|
|
330
|
-
on the day both were adopted, this lint passed a `plugin.json` the platform refused, and the platform
|
|
331
|
-
passed three separately-broken skills this lint fails. Recorded as
|
|
332
|
-
[`../.portulan/memory/a-checkers-coverage-is-measured-not-named.md`](../.portulan/memory/a-checkers-coverage-is-measured-not-named.md).
|
|
333
|
-
|
|
334
|
-
Deliberately **not** checked: the platform's reserved-marketplace-name list, which the platform re-checks
|
|
335
|
-
on every load and has already changed — a copy frozen here would drift into a false verdict in one
|
|
336
|
-
direction or the other.
|
|
337
|
-
|
|
338
|
-
## Why it lives here rather than in the workspace
|
|
339
|
-
|
|
340
|
-
`plugin-lint` is the harder case of the two, and it is placed here on the same rule with a caveat worth
|
|
341
|
-
stating: it validates *any* plugin root, so it is product surface in shape — but the invariants it
|
|
342
|
-
enforces are this repository's own, and a customer's plugin may reasonably differ. Treat it as a tool
|
|
343
|
-
this repository owns and others may run, not as a contract shipped to them.
|
|
344
|
-
|
|
345
|
-
`doctor` validates *any* workspace, so it is product surface rather than customer-zero tooling —
|
|
346
|
-
[`../.portulan/verify/`](../.portulan/verify/) holds this repository's own recipes and
|
|
347
|
-
[`../.portulan/tools/`](../.portulan/tools/) holds operator tooling. Putting a universal validator in
|
|
348
|
-
either would have made it look local to the one workspace it happened to be written beside.
|
|
349
|
-
|
|
350
|
-
Zero-dependency ESM on Node rather than TypeScript, and that is **a ruling rather than a "for
|
|
351
|
-
now"**: a build step would mean this repository could no longer be checked by cloning it. So the CLI
|
|
352
|
-
does **not** bring a build with it. `package.json` declares no dependencies and exists to carry the
|
|
353
|
-
`bin`; `npm install` fetches nothing, and every tool here still runs as `node cli/<tool>.mjs` from a
|
|
354
|
-
fresh clone.
|
|
355
|
-
|
|
356
|
-
## Environment the compiled runners read
|
|
37
|
+
| File | What it is |
|
|
38
|
+
|---|---|
|
|
39
|
+
| [`gate.mjs`](gate.mjs) | The PreToolUse gate runner — the *explanation* half of the enforcement compiler. |
|
|
40
|
+
| [`stop-gate.mjs`](stop-gate.mjs) | The Stop-gate runner — "done" is not a thing an agent may simply declare. |
|
|
41
|
+
| [`advisory.mjs`](advisory.mjs) | The restart advisory — one line, once, where the agent is, and the same figure for the human. |
|
|
357
42
|
|
|
358
|
-
|
|
359
|
-
where the workspace is rather than deriving it from where they sit — which is what lets them ship in the
|
|
360
|
-
package at all (before milestone 7 they resolved `HERE/..`, which is true in exactly one layout).
|
|
43
|
+
### Tools and modules
|
|
361
44
|
|
|
362
|
-
|
|
363
|
-
|---|---|---|
|
|
364
|
-
| `CLAUDE_PROJECT_DIR` | the repository the policy governs; the host sets it, and the emitted hook interpolates it | falls back to the process's working directory, which is where a hook runs |
|
|
365
|
-
| `PORTULAN_WORKSPACE` | the workspace directory's **name** inside that repository | `.portulan` |
|
|
45
|
+
Run as `node cli/<file>.mjs`, or imported by the files above.
|
|
366
46
|
|
|
367
|
-
|
|
47
|
+
| File | What it is |
|
|
48
|
+
|---|---|
|
|
49
|
+
| [`ab-grade.mjs`](ab-grade.mjs) | The A/B graders — what an arm's tree says it did, and the discrimination that proves a grader read it. Not shipped. |
|
|
50
|
+
| [`ab-run.mjs`](ab-run.mjs) | The A/B runner: spawns the agent turns of both arms and records the baseline they measure. Not shipped. |
|
|
51
|
+
| [`ab.mjs`](ab.mjs) | The A/B arm builder — what "Portulan on" is made of, built rather than described. Not shipped. |
|
|
52
|
+
| [`comments.mjs`](comments.mjs) | `comments` — the comments in a tree that record a change's history instead of what the code cannot show. |
|
|
53
|
+
| [`context.mjs`](context.mjs) | What a context loads, measured: what a boot reads in full, and what the host loads into every context. |
|
|
54
|
+
| [`control-chars.mjs`](control-chars.mjs) | `control-chars` — the rail on bytes a reader cannot see. |
|
|
55
|
+
| [`discover.mjs`](discover.mjs) | Host plugin-cache discovery: reads a host's installed-plugin record to resolve a pointer and find pack roots. |
|
|
56
|
+
| [`drills.mjs`](drills.mjs) | The forced-red drill harness — every rail broken on purpose, and required to fire. |
|
|
57
|
+
| [`eval-bundle.mjs`](eval-bundle.mjs) | Cut a named-recipient evaluation bundle of Portulan from a commit. Not shipped. |
|
|
58
|
+
| [`finish.mjs`](finish.mjs) | The finishing command — one call closes a change: its recipes, its changelog fragment, its commit, its push. |
|
|
59
|
+
| [`form.mjs`](form.mjs) | `form` — which form a consumer's records and boot are in, and the files that move them to the new one. |
|
|
60
|
+
| [`fuzz-shell.mjs`](fuzz-shell.mjs) | Grammar-aware fuzzing over this repository's two shell segmenters. |
|
|
61
|
+
| [`goldens.mjs`](goldens.mjs) | Grade this workspace's compiled gates against a corpus of adversarial fixtures. |
|
|
62
|
+
| [`inside.mjs`](inside.mjs) | Is one path inside another? One predicate, one file, no dependencies. |
|
|
63
|
+
| [`instructions.mjs`](instructions.mjs) | `instructions` — the sections a team marks in its own instruction file, moved to on-read units and proved. |
|
|
64
|
+
| [`ledger.mjs`](ledger.mjs) | What a change spends, measured from the host's own usage records. |
|
|
65
|
+
| [`librarian.mjs`](librarian.mjs) | `librarian` — the scheduled pass over the curated layer: reindex, staleness, nags, demotion drafts. |
|
|
66
|
+
| [`local-routing.mjs`](local-routing.mjs) | Verify the shipped operational local roles against their immutable acquired A/B record. No model. Not shipped. |
|
|
67
|
+
| [`manifest.mjs`](manifest.mjs) | The two facts every tool here needs out of `package.json`, read once and in a module that imports nothing of ours. |
|
|
68
|
+
| [`mutants.mjs`](mutants.mjs) | Mutation-test this repository's two gate matchers against the corpus that claims to cover them. |
|
|
69
|
+
| [`pack-identity.mjs`](pack-identity.mjs) | Every file `npm pack` emits is byte-identical to the tracked blob it came from. |
|
|
70
|
+
| [`pack-version.mjs`](pack-version.mjs) | A change to a pack's `contributes` must move that pack's `portulan.version`. |
|
|
71
|
+
| [`payload.mjs`](payload.mjs) | Every `cli/*.mjs` the npm payload carries is classified, and nothing arrives in it unclassified. Not shipped. |
|
|
72
|
+
| [`plugin-lint.mjs`](plugin-lint.mjs) | `plugin-lint` — the packaging validator. |
|
|
73
|
+
| [`recipe-set.mjs`](recipe-set.mjs) | The recipe set: the verify recipes a workspace yields, its own plus each composed pack's, namespaced by pack. |
|
|
74
|
+
| [`release-eval.mjs`](release-eval.mjs) | Portulan — the eval result a release carries. |
|
|
75
|
+
| [`review-meter.mjs`](review-meter.mjs) | The review-loop meter: the historical figures of this repository's Copilot review loop, derived from its reviews. |
|
|
76
|
+
| [`roster.mjs`](roster.mjs) | The cli roster: `cli/README.md`, rendered from the files it lists and never edited by hand. Not shipped. |
|
|
77
|
+
| [`rule-carriers.mjs`](rule-carriers.mjs) | `rule-carriers` — the rail that keeps a reduced rule reduced. |
|
|
78
|
+
| [`sessions.mjs`](sessions.mjs) | The session switches' texts: the cache-lifetime offer `init` and `upgrade` print, and the one line `doctor` reports on them. |
|
|
79
|
+
| [`skill-goldens.mjs`](skill-goldens.mjs) | The core-skill golden corpus — every mandate a core skill states, bound to the live artifacts it governs, and graded. |
|
|
80
|
+
| [`skills-set.mjs`](skills-set.mjs) | The registrable set: the `skills` paths a plugin manifest must declare so a composed pack's skills register. |
|
|
81
|
+
| [`symbols.mjs`](symbols.mjs) | A code file's outline, one line per symbol, and a Markdown file's, one line per heading with its size. |
|
|
82
|
+
| [`telemetry.mjs`](telemetry.mjs) | The OTel emitter: the review-loop figures as OTLP/HTTP JSON, sent only when a committed config opts in. |
|
|
83
|
+
| [`version-carriers.mjs`](version-carriers.mjs) | Every prose statement of the current version agrees with `package.json`. |
|
|
84
|
+
| [`warm.mjs`](warm.mjs) | The warm-start A/B: a fresh session's cost when an earlier one left its prefix in the prompt cache, against cold. Not shipped. |
|
|
85
|
+
|
|
86
|
+
### Tests
|
|
87
|
+
|
|
88
|
+
Node's own runner: `node --test "cli/**/*.test.mjs"`, which the `tests` recipe runs.
|
|
368
89
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
90
|
+
| File | What it is |
|
|
91
|
+
|---|---|
|
|
92
|
+
| [`ab-grade.test.mjs`](ab-grade.test.mjs) | Tests for `ab-grade` — the A/B graders: staging, attribution, the four scenarios, the two levels and the register. |
|
|
93
|
+
| [`ab-run.test.mjs`](ab-run.test.mjs) | Tests for `ab-run` — the A/B runner: isolation, the matrix, the journal, the register and the checks behind it. |
|
|
94
|
+
| [`ab.test.mjs`](ab.test.mjs) | Tests for `ab` — the A/B arm builder: its disposition table, rule-2 matcher, nonces, isolation and stop probe. |
|
|
95
|
+
| [`advisory.test.mjs`](advisory.test.mjs) | Tests for `advisory` — the restart advisory's one line with a tool result or at the prompt, and its figure in the status line. |
|
|
96
|
+
| [`branch-freshness.test.mjs`](branch-freshness.test.mjs) | Tests for this repository's branch freshness check and trusted workflow. |
|
|
97
|
+
| [`checkout-refs.live.test.mjs`](checkout-refs.live.test.mjs) | A workflow that checks out MORE THAN ONCE names the ref of every checkout after the first. |
|
|
98
|
+
| [`cli-roster.live.test.mjs`](cli-roster.live.test.mjs) | `cli/README.md` is exactly what ./roster.mjs renders from this directory, and the render keeps its shape. |
|
|
99
|
+
| [`collisions.test.mjs`](collisions.test.mjs) | The collision contract: `init`, `new` and `vendor` answer each contracted state alike, and no fourth `collisions` exists. |
|
|
100
|
+
| [`comments.test.mjs`](comments.test.mjs) | Tests for `comments`: each language's comments found exactly, and the four kinds of history told from the rest. |
|
|
101
|
+
| [`compile.test.mjs`](compile.test.mjs) | Tests for `compile` — the enforcement compiler. |
|
|
102
|
+
| [`context.test.mjs`](context.test.mjs) | Tests for `context` — what a boot reads and what the host loads into every context, measured. |
|
|
103
|
+
| [`control-chars.test.mjs`](control-chars.test.mjs) | Tests for `control-chars` — the rail on bytes a reader cannot see. |
|
|
104
|
+
| [`discover.test.mjs`](discover.test.mjs) | Tests for host plugin-cache discovery. |
|
|
105
|
+
| [`doctor.test.mjs`](doctor.test.mjs) | Tests for `doctor` — workspace validation, branch-owned hybrid usage and static-mode isolation. |
|
|
106
|
+
| [`drills.test.mjs`](drills.test.mjs) | Tests for `drills` — the forced-red drill harness: its table, perturbation guards, pair oracle, hook rails and tree choice. |
|
|
107
|
+
| [`eval-bundle.test.mjs`](eval-bundle.test.mjs) | The evaluation-bundle cutter, driven on this repository AND on real fixture repositories. |
|
|
108
|
+
| [`feedback.live.test.mjs`](feedback.live.test.mjs) | The sender's field map, against the real issue forms rather than against a fixture. |
|
|
109
|
+
| [`feedback.test.mjs`](feedback.test.mjs) | Tests for `portulan feedback`: draft, preview, send, the seam scan and the payload; `gh` is injected, never run. |
|
|
110
|
+
| [`finish.test.mjs`](finish.test.mjs) | Tests for `finish` — one call closes a change: the fragment, the commit, every recipe on it, the push. |
|
|
111
|
+
| [`form.test.mjs`](form.test.mjs) | `form` — the one definition of a consumer's new form, which `init`, `vendor`, `upgrade` and `doctor` read. |
|
|
112
|
+
| [`fuzz-shell.ground.test.mjs`](fuzz-shell.ground.test.mjs) | Tests for `fuzz-shell`'s ground truth, measured under real bash: each bash-safe production's position, the path spellings, the wrappers. |
|
|
113
|
+
| [`fuzz-shell.test.mjs`](fuzz-shell.test.mjs) | Tests for `fuzz-shell` — the hermetic half: its payloads are gated commands, so nothing here spawns bash or runs one. |
|
|
114
|
+
| [`gate.test.mjs`](gate.test.mjs) | The PreToolUse gate runner, driven as the host drives it. |
|
|
115
|
+
| [`gh-bot.test.mjs`](gh-bot.test.mjs) | The agent identity's wrapper refuses the endpoints it is not for. |
|
|
116
|
+
| [`goldens.test.mjs`](goldens.test.mjs) | Tests for `goldens` — the gate-corpus rail: the exemption, both rails, every corpus refusal and the CLI. |
|
|
117
|
+
| [`index.test.mjs`](index.test.mjs) | Tests for `index` — the memory, handoff and scope indexes, the memory budgets, and changelog fragments. |
|
|
118
|
+
| [`init.test.mjs`](init.test.mjs) | Tests for `init` — the onboarding subcommand that drafts a workspace for a repository that has none. |
|
|
119
|
+
| [`instructions.test.mjs`](instructions.test.mjs) | Tests for `instructions`: the sections a team marks, moved to on-read units and proved, and the join back. |
|
|
120
|
+
| [`ledger.test.mjs`](ledger.test.mjs) | Tests for `ledger` — what a change spends, from the host's own usage records. |
|
|
121
|
+
| [`librarian.test.mjs`](librarian.test.mjs) | Tests for `librarian` — the scheduled pass over the curated layer. |
|
|
122
|
+
| [`list-quoting.live.test.mjs`](list-quoting.live.test.mjs) | Every recipe that enumerates the tree reads the pathname git actually carries. |
|
|
123
|
+
| [`mutants.test.mjs`](mutants.test.mjs) | Tests for `mutants`, the mutation-census rail: its operator table, its imports, its corpus and its refusals. |
|
|
124
|
+
| [`new.test.mjs`](new.test.mjs) | Tests for `new`: scaffolds that validate, written into the user's own layer and never into `core/`. |
|
|
125
|
+
| [`pack-identity.test.mjs`](pack-identity.test.mjs) | Tests for the byte-identity rail: green, both findings and could-not-run, each exercised. |
|
|
126
|
+
| [`pack-local-llm.test.mjs`](pack-local-llm.test.mjs) | Tests for the `tools/local-llm` pack's runner — its loopback, input-budget and rollback rails, its eval screen, dedup ranking and A/B record. |
|
|
127
|
+
| [`pack-version.test.mjs`](pack-version.test.mjs) | The pack-version rail, driven on REAL git repositories rather than an injected history. |
|
|
128
|
+
| [`payload.test.mjs`](payload.test.mjs) | Tests for the payload-classification rail. |
|
|
129
|
+
| [`pinned-roots.live.test.mjs`](pinned-roots.live.test.mjs) | Required checks name their resolution root, and every suite whose imports reach the host's plugin record empties it. |
|
|
130
|
+
| [`plugin-lint.test.mjs`](plugin-lint.test.mjs) | Tests for `plugin-lint` — the packaging validator. |
|
|
131
|
+
| [`portulan.test.mjs`](portulan.test.mjs) | Tests for the `npx` entry point: dispatch through an injected loader, its refusals, and its version. |
|
|
132
|
+
| [`recipe-set.live.test.mjs`](recipe-set.live.test.mjs) | The composed recipe set, against THIS repository rather than against fixtures. |
|
|
133
|
+
| [`recipe-set.test.mjs`](recipe-set.test.mjs) | The recipe set — one carrier, and every reader reaches it. |
|
|
134
|
+
| [`release-eval.test.mjs`](release-eval.test.mjs) | The release-eval suite, on the record layer only: `--capture` spawns every other rail, the tests that run this suite included. |
|
|
135
|
+
| [`review-meter.test.mjs`](review-meter.test.mjs) | Tests for `review-meter` — the review-loop meter: arithmetic, snapshot contract, register rail, window and shaping. |
|
|
136
|
+
| [`rule-carriers.test.mjs`](rule-carriers.test.mjs) | Tests for `rule-carriers` — the rail that keeps a reduced rule reduced. |
|
|
137
|
+
| [`sessions.test.mjs`](sessions.test.mjs) | `sessions` — the cache lifetime's offer, and `doctor`'s one line on the session switches. |
|
|
138
|
+
| [`shadowed-pack.test.mjs`](shadowed-pack.test.mjs) | A shadowed pack is refused rather than picked. |
|
|
139
|
+
| [`shadowed-resolution.test.mjs`](shadowed-resolution.test.mjs) | `index` and `recipe-set` refuse a shadowed pack, and the divergence they would otherwise ship. |
|
|
140
|
+
| [`skill-goldens.test.mjs`](skill-goldens.test.mjs) | Tests for `skill-goldens`, the core-skill golden corpus: every refusal exercised on a fixture that trips it. |
|
|
141
|
+
| [`skills-set.live.test.mjs`](skills-set.live.test.mjs) | The registrable set, against THIS repository rather than against fixtures. |
|
|
142
|
+
| [`skills-set.test.mjs`](skills-set.test.mjs) | The registrable set — what a plugin manifest must declare so a composed pack's skills register. |
|
|
143
|
+
| [`stop-gate.test.mjs`](stop-gate.test.mjs) | Tests for the Stop-gate runner: its per-reason caps, the handoff date, its verdicts, the did-work signals and the tree it answers about. |
|
|
144
|
+
| [`symbols.live.test.mjs`](symbols.live.test.mjs) | `symbols` against this repository: tracked code and Markdown files outline, each declaration compiles alone, and heading links land. |
|
|
145
|
+
| [`symbols.test.mjs`](symbols.test.mjs) | The outline's contract, on fixtures: what each construct prints, and every refusal. |
|
|
146
|
+
| [`telemetry.test.mjs`](telemetry.test.mjs) | Tests for the OTel emitter: its consent gate, closed payload, offline audit and transport. |
|
|
147
|
+
| [`test-isolation.live.test.mjs`](test-isolation.live.test.mjs) | A test that substitutes a shared object hands the restore to the runner, or says why it cannot. |
|
|
148
|
+
| [`upgrade.live.test.mjs`](upgrade.live.test.mjs) | `upgrade` against real workspaces, drafted by the real `init`, never against hand-built fixtures. |
|
|
149
|
+
| [`upgrade.test.mjs`](upgrade.test.mjs) | `upgrade` — the migration chain, and the three kinds of step. |
|
|
150
|
+
| [`vendor.test.mjs`](vendor.test.mjs) | Tests for `vendor` — a workspace materialised where it is needed, and the residence switch in both directions. |
|
|
151
|
+
| [`version-carriers.test.mjs`](version-carriers.test.mjs) | Tests for `version-carriers`: green, drift, a reworded carrier and could-not-run, each exercised. |
|
|
152
|
+
| [`vision-sections.live.test.mjs`](vision-sections.live.test.mjs) | The constitution is cited by its sections, not by its words, and this holds every citation to it. |
|
|
153
|
+
| [`warm.test.mjs`](warm.test.mjs) | Tests for the warm-start A/B: pricing, Portulan's share, the child's start, sequences and switches, on a stub agent. |
|
|
154
|
+
|
|
155
|
+
### Other files
|
|
156
|
+
|
|
157
|
+
What the tools and suites read.
|
|
375
158
|
|
|
159
|
+
| File | What it is |
|
|
160
|
+
|---|---|
|
|
161
|
+
| [`eval-license.template.md`](eval-license.template.md) | Portulan Evaluation Issue (`EVAL-LICENSE.md`). Not shipped. |
|
|
162
|
+
| [`fixtures/`](fixtures/) | The fixtures the suites build on; each suite names the ones it reads. Not shipped. |
|